From 5765982a4d040757576a70a7db57809f80c35ba5 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Fri, 29 May 2026 15:45:31 +0200 Subject: [PATCH] Remove tests --- .../Configuration/RoutesTests.cs | 86 ------ .../Domain/TrackTests.cs | 89 ------ .../Extensions/ResultExtensionsTests.cs | 74 ----- .../CreatePlayListCommandHandlerTests.cs | 105 ------- .../CreateTrackCommandHandlerTests.cs | 141 ---------- .../Handlers/CreateUserCommandHandlerTests.cs | 44 --- .../DeletePlayListCommandHandlerTests.cs | 88 ------ .../DeleteTrackCommandHandlerTests.cs | 108 -------- .../Handlers/GetByIdQueryHandlerTests.cs | 93 ------- .../GetTracksByUserIdQueryHandlerTests.cs | 83 ------ .../Handlers/ListQueryHandlerTests.cs | 135 --------- ...layListPictureUploadCommandHandlerTests.cs | 129 --------- ...questTrackUploadUrlsCommandHandlerTests.cs | 134 --------- .../UpdatePlayListCommandHandlerTests.cs | 144 ---------- .../Infrastructure/SchemaSmokeTests.cs | 42 --- .../Infrastructure/SqliteTestContext.cs | 44 --- .../Infrastructure/TestData.cs | 75 ----- .../Infrastructure/TestDatabase.cs | 39 --- .../Musify.Application.Tests.csproj | 29 -- .../Pagination/PaginatedResponseTests.cs | 38 --- .../Services/UploadIntentValidatorTests.cs | 256 ------------------ ...ify.Infrastructure.IntegrationTests.csproj | 28 -- .../SeaweedFsFixture.cs | 67 ----- .../StorageServiceTests.cs | 145 ---------- .../JobTestHarness.cs | 32 --- .../Musify.Infrastructure.Tests.csproj | 29 -- .../TemporalUploadsCleanUpJobTests.cs | 72 ----- .../UploadIntentExpirationJobTests.cs | 84 ------ 28 files changed, 2433 deletions(-) delete mode 100644 Tests/Musify.Application.Tests/Configuration/RoutesTests.cs delete mode 100644 Tests/Musify.Application.Tests/Domain/TrackTests.cs delete mode 100644 Tests/Musify.Application.Tests/Extensions/ResultExtensionsTests.cs delete mode 100644 Tests/Musify.Application.Tests/Handlers/CreatePlayListCommandHandlerTests.cs delete mode 100644 Tests/Musify.Application.Tests/Handlers/CreateTrackCommandHandlerTests.cs delete mode 100644 Tests/Musify.Application.Tests/Handlers/CreateUserCommandHandlerTests.cs delete mode 100644 Tests/Musify.Application.Tests/Handlers/DeletePlayListCommandHandlerTests.cs delete mode 100644 Tests/Musify.Application.Tests/Handlers/DeleteTrackCommandHandlerTests.cs delete mode 100644 Tests/Musify.Application.Tests/Handlers/GetByIdQueryHandlerTests.cs delete mode 100644 Tests/Musify.Application.Tests/Handlers/GetTracksByUserIdQueryHandlerTests.cs delete mode 100644 Tests/Musify.Application.Tests/Handlers/ListQueryHandlerTests.cs delete mode 100644 Tests/Musify.Application.Tests/Handlers/RequestPlayListPictureUploadCommandHandlerTests.cs delete mode 100644 Tests/Musify.Application.Tests/Handlers/RequestTrackUploadUrlsCommandHandlerTests.cs delete mode 100644 Tests/Musify.Application.Tests/Handlers/UpdatePlayListCommandHandlerTests.cs delete mode 100644 Tests/Musify.Application.Tests/Infrastructure/SchemaSmokeTests.cs delete mode 100644 Tests/Musify.Application.Tests/Infrastructure/SqliteTestContext.cs delete mode 100644 Tests/Musify.Application.Tests/Infrastructure/TestData.cs delete mode 100644 Tests/Musify.Application.Tests/Infrastructure/TestDatabase.cs delete mode 100644 Tests/Musify.Application.Tests/Musify.Application.Tests.csproj delete mode 100644 Tests/Musify.Application.Tests/Pagination/PaginatedResponseTests.cs delete mode 100644 Tests/Musify.Application.Tests/Services/UploadIntentValidatorTests.cs delete mode 100644 Tests/Musify.Infrastructure.IntegrationTests/Musify.Infrastructure.IntegrationTests.csproj delete mode 100644 Tests/Musify.Infrastructure.IntegrationTests/SeaweedFsFixture.cs delete mode 100644 Tests/Musify.Infrastructure.IntegrationTests/StorageServiceTests.cs delete mode 100644 Tests/Musify.Infrastructure.Tests/JobTestHarness.cs delete mode 100644 Tests/Musify.Infrastructure.Tests/Musify.Infrastructure.Tests.csproj delete mode 100644 Tests/Musify.Infrastructure.Tests/TemporalUploadsCleanUpJobTests.cs delete mode 100644 Tests/Musify.Infrastructure.Tests/UploadIntentExpirationJobTests.cs diff --git a/Tests/Musify.Application.Tests/Configuration/RoutesTests.cs b/Tests/Musify.Application.Tests/Configuration/RoutesTests.cs deleted file mode 100644 index a0c0cbf..0000000 --- a/Tests/Musify.Application.Tests/Configuration/RoutesTests.cs +++ /dev/null @@ -1,86 +0,0 @@ -using FluentAssertions; -using Musify.Application.Configuration; -using Xunit; - -namespace Musify.Application.Tests.Configuration; - -public sealed class RoutesTests -{ - private static readonly Guid UserId = Guid.Parse("11111111-1111-1111-1111-111111111111"); - - [Fact] - public void TrackRoutes_default_picture_and_audio_paths() - { - var routes = new TrackRoutes(); - - routes.OriginalPicturesPath.Should().Be("Tracks/OriginalPictures"); - routes.OriginalAudiosPath.Should().Be("Tracks/OriginalAudios"); - routes.ProcessedAudiosPath.Should().Be("Tracks/ProcessedAudios"); - } - - [Fact] - public void TrackRoutes_BuildOriginalPicturePath_with_user_nests_under_uploads() - { - var routes = new TrackRoutes(); - - routes.BuildOriginalPicturePath(UserId, "cover.webp") - .Should().Be($"uploads/{UserId}/Tracks/OriginalPictures/cover.webp"); - } - - [Fact] - public void TrackRoutes_BuildOriginalAudioPath_with_user_nests_under_uploads() - { - var routes = new TrackRoutes(); - - routes.BuildOriginalAudioPath(UserId, "song.mp3") - .Should().Be($"uploads/{UserId}/Tracks/OriginalAudios/song.mp3"); - } - - [Fact] - public void TrackRoutes_BuildTempPicturePath_uses_prefix_user_and_parent_folder() - { - var routes = new TrackRoutes(); - - routes.BuildTempPicturePath("temp", UserId, "obj.png") - .Should().Be($"temp/{UserId}/Tracks/obj.png"); - } - - [Fact] - public void CombineKey_trims_slashes_and_whitespace_per_segment() - { - var routes = new TrackRoutes(); - - var result = routes.BuildTempPicturePath(" /temp/ ", UserId, "/obj.png\\"); - - result.Should().Be($"temp/{UserId}/Tracks/obj.png"); - } - - [Fact] - public void CombineKey_drops_empty_and_whitespace_segments() - { - var routes = new TrackRoutes(); - - var result = routes.BuildTempPicturePath(" ", UserId, "obj.png"); - - result.Should().Be($"{UserId}/Tracks/obj.png"); - } - - [Fact] - public void PlayListRoutes_BuildOriginalPicturePath_with_user_nests_under_uploads() - { - var routes = new PlayListRoutes(); - - routes.BuildOriginalPicturePath(UserId, "cover.webp") - .Should().Be($"uploads/{UserId}/PlayLists/OriginalPictures/cover.webp"); - } - - [Fact] - public void PlayListRoutes_size_paths_use_parent_folder() - { - var routes = new PlayListRoutes(); - - routes.SmallPicturesPath.Should().Be("PlayLists/SmallPictures"); - routes.MediumPicturesPath.Should().Be("PlayLists/MediumPictures"); - routes.LargePicturesPath.Should().Be("PlayLists/LargePictures"); - } -} diff --git a/Tests/Musify.Application.Tests/Domain/TrackTests.cs b/Tests/Musify.Application.Tests/Domain/TrackTests.cs deleted file mode 100644 index f56505d..0000000 --- a/Tests/Musify.Application.Tests/Domain/TrackTests.cs +++ /dev/null @@ -1,89 +0,0 @@ -using FluentAssertions; -using Musify.Domain.ValueObjects; -using Musify.Application.Tests.Infrastructure; -using Xunit; - -namespace Musify.Application.Tests.Domain; - -public sealed class TrackTests -{ - [Fact] - public void IsPicturesProcessed_is_true_only_when_completed() - { - var track = TestData.Track(); - - track.PicturesProcessingStatus = ProcessingStatus.Completed; - track.IsPicturesProcessed.Should().BeTrue(); - - track.PicturesProcessingStatus = ProcessingStatus.Processing; - track.IsPicturesProcessed.Should().BeFalse(); - - track.PicturesProcessingStatus = ProcessingStatus.Failed; - track.IsPicturesProcessed.Should().BeFalse(); - } - - [Fact] - public void IsAudioProcessed_is_true_only_when_completed() - { - var track = TestData.Track(); - - track.AudioTranscodeProcessingStatus = ProcessingStatus.Completed; - track.IsAudioProcessed.Should().BeTrue(); - - track.AudioTranscodeProcessingStatus = ProcessingStatus.Pending; - track.IsAudioProcessed.Should().BeFalse(); - } - - [Fact] - public void Defaults_are_active_pending_and_zero_retries() - { - var track = new Musify.Domain.Entities.Track - { - Title = "t", - NormalizedTitle = "T", - OriginalPictureName = "p.webp", - OriginalAudioName = "a.mp3", - }; - - track.LifeCycleStatus.Should().Be(LifeCycleStatus.Active); - track.PicturesProcessingStatus.Should().Be(ProcessingStatus.Pending); - track.AudioTranscodeProcessingStatus.Should().Be(ProcessingStatus.Pending); - track.RetryCount.Should().Be(0); - track.Id.Should().NotBe(Guid.Empty); - } -} - -public sealed class EnumValueRegressionTests -{ - [Fact] - public void ProcessingStatus_values_are_stable() - { - ((int)ProcessingStatus.Pending).Should().Be(0); - ((int)ProcessingStatus.Processing).Should().Be(1); - ((int)ProcessingStatus.Completed).Should().Be(2); - ((int)ProcessingStatus.Failed).Should().Be(3); - } - - [Fact] - public void LifeCycleStatus_values_are_stable() - { - ((int)LifeCycleStatus.Active).Should().Be(0); - ((int)LifeCycleStatus.Removing).Should().Be(1); - } - - [Fact] - public void UploadIntentStatus_values_are_stable() - { - ((int)UploadIntentStatus.Issued).Should().Be(0); - ((int)UploadIntentStatus.Consumed).Should().Be(1); - ((int)UploadIntentStatus.Expired).Should().Be(2); - } - - [Fact] - public void UploadIntentPurpose_values_are_stable() - { - ((int)UploadIntentPurpose.PlayListPicture).Should().Be(0); - ((int)UploadIntentPurpose.TrackPicture).Should().Be(1); - ((int)UploadIntentPurpose.TrackAudio).Should().Be(2); - } -} diff --git a/Tests/Musify.Application.Tests/Extensions/ResultExtensionsTests.cs b/Tests/Musify.Application.Tests/Extensions/ResultExtensionsTests.cs deleted file mode 100644 index 72ef974..0000000 --- a/Tests/Musify.Application.Tests/Extensions/ResultExtensionsTests.cs +++ /dev/null @@ -1,74 +0,0 @@ -using Ardalis.Result; -using FluentAssertions; -using Musify.Application.Extensions; -using Xunit; - -namespace Musify.Application.Tests.Extensions; - -public sealed class ResultExtensionsTests -{ - [Fact] - public void NotFound_maps_to_NotFound_and_preserves_errors() - { - var source = Result.NotFound("missing"); - - var mapped = source.As(); - - mapped.Status.Should().Be(ResultStatus.NotFound); - mapped.Errors.Should().Contain("missing"); - } - - [Fact] - public void Conflict_maps_to_Conflict() - { - Result.Conflict("dup").As().Status.Should().Be(ResultStatus.Conflict); - } - - [Fact] - public void Unauthorized_maps_to_Unauthorized() - { - Result.Unauthorized().As().Status.Should().Be(ResultStatus.Unauthorized); - } - - [Fact] - public void Forbidden_maps_to_Forbidden() - { - Result.Forbidden().As().Status.Should().Be(ResultStatus.Forbidden); - } - - [Fact] - public void Invalid_maps_to_Invalid_and_preserves_validation_errors() - { - var source = Result.Invalid(new ValidationError("field", "bad")); - - var mapped = source.As(); - - mapped.Status.Should().Be(ResultStatus.Invalid); - mapped.ValidationErrors.Should().ContainSingle(e => e.ErrorMessage == "bad"); - } - - [Fact] - public void Error_maps_to_Error() - { - Result.Error("boom").As().Status.Should().Be(ResultStatus.Error); - } - - [Theory] - [InlineData(ResultStatus.Ok)] - [InlineData(ResultStatus.Created)] - [InlineData(ResultStatus.NoContent)] - public void Successful_results_throw_when_converted(ResultStatus status) - { - Result source = status switch - { - ResultStatus.Ok => Result.Success(1), - ResultStatus.Created => Result.Created(1), - ResultStatus.NoContent => Result.NoContent(), - _ => throw new InvalidOperationException() - }; - - var act = () => source.As(); - - act.Should().Throw(); - } -} diff --git a/Tests/Musify.Application.Tests/Handlers/CreatePlayListCommandHandlerTests.cs b/Tests/Musify.Application.Tests/Handlers/CreatePlayListCommandHandlerTests.cs deleted file mode 100644 index 6f60132..0000000 --- a/Tests/Musify.Application.Tests/Handlers/CreatePlayListCommandHandlerTests.cs +++ /dev/null @@ -1,105 +0,0 @@ -using Ardalis.Result; -using FluentAssertions; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging.Abstractions; -using Musify.Application.Contracts; -using Musify.Application.Events; -using Musify.Application.PlayLists.Commands; -using Musify.Application.PlayLists.Handlers; -using Musify.Application.Services; -using Musify.Application.Tests.Infrastructure; -using Musify.Domain.ValueObjects; -using NSubstitute; -using Xunit; - -namespace Musify.Application.Tests.Handlers; - -public sealed class CreatePlayListCommandHandlerTests -{ - private readonly IEventBus _eventBus = Substitute.For(); - private readonly IStorageService _storage = Substitute.For(); - - private CreatePlayListCommandHandler CreateSut(TestDatabase db) => - new( - _eventBus, - db, - new UploadIntentValidator(db, _storage), - NullLogger.Instance, - TestData.StorageConfig(), - TestData.PlayListConfig(), - TestData.UploadIntentConfig()); - - [Fact] - public async Task When_user_missing_returns_not_found() - { - await using var ctx = new SqliteTestContext(); - var sut = CreateSut(ctx.Database); - - var result = await sut.Handle( - new CreatePlayListCommand(Guid.NewGuid(), "List", "desc", null), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.NotFound); - } - - [Fact] - public async Task Without_picture_intent_uses_preset_and_does_not_publish() - { - await using var ctx = new SqliteTestContext(); - var user = TestData.User(); - ctx.Database.Users.Add(user); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = CreateSut(ctx.Database); - var result = await sut.Handle( - new CreatePlayListCommand(user.Id, " Chill Vibes ", "desc", null), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Created); - await _eventBus.DidNotReceive().PublishAsync(Arg.Any(), Arg.Any()); - - var persisted = await ctx.NewContext().PlayLists.SingleAsync(p => p.UserId == user.Id); - persisted.OriginalPictureName.Should().Be(TestData.PlayListConfig().Routes.PresetOriginalPicture); - persisted.NormalizedName.Should().Be("CHILL VIBES"); - } - - [Fact] - public async Task With_valid_picture_intent_publishes_event_with_intent_object_name() - { - await using var ctx = new SqliteTestContext(); - var user = TestData.User(); - var intent = TestData.UploadIntent(user.Id, UploadIntentPurpose.PlayListPicture, objectName: "cover.webp"); - ctx.Database.Users.Add(user); - ctx.Database.UploadIntents.Add(intent); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - _storage.HeadObjectAsync(intent.Bucket, intent.Key, Arg.Any()) - .Returns(new ObjectMetaData("image/webp", 500)); - - var sut = CreateSut(ctx.Database); - var result = await sut.Handle( - new CreatePlayListCommand(user.Id, "List", "desc", intent.Id), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Created); - await _eventBus.Received(1).PublishAsync( - Arg.Is(e => e.PictureIntentId == intent.Id), - Arg.Any()); - - var persisted = await ctx.NewContext().PlayLists.SingleAsync(p => p.UserId == user.Id); - persisted.OriginalPictureName.Should().Be("cover.webp"); - } - - [Fact] - public async Task With_invalid_picture_intent_propagates_error_and_does_not_persist() - { - await using var ctx = new SqliteTestContext(); - var user = TestData.User(); - ctx.Database.Users.Add(user); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = CreateSut(ctx.Database); - var result = await sut.Handle( - new CreatePlayListCommand(user.Id, "List", "desc", Guid.NewGuid()), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.NotFound); - (await ctx.NewContext().PlayLists.CountAsync()).Should().Be(0); - } -} diff --git a/Tests/Musify.Application.Tests/Handlers/CreateTrackCommandHandlerTests.cs b/Tests/Musify.Application.Tests/Handlers/CreateTrackCommandHandlerTests.cs deleted file mode 100644 index 640b45b..0000000 --- a/Tests/Musify.Application.Tests/Handlers/CreateTrackCommandHandlerTests.cs +++ /dev/null @@ -1,141 +0,0 @@ -using Ardalis.Result; -using FluentAssertions; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging.Abstractions; -using Musify.Application.Contracts; -using Musify.Application.Events; -using Musify.Application.Services; -using Musify.Application.Tests.Infrastructure; -using Musify.Application.Tracks.Commands; -using Musify.Application.Tracks.Handlers; -using Musify.Domain.Entities; -using Musify.Domain.ValueObjects; -using NSubstitute; -using Xunit; - -namespace Musify.Application.Tests.Handlers; - -public sealed class CreateTrackCommandHandlerTests -{ - private readonly IEventBus _eventBus = Substitute.For(); - private readonly IStorageService _storage = Substitute.For(); - - private CreateTrackCommandHandler CreateSut(TestDatabase db) => - new( - db, - _eventBus, - new UploadIntentValidator(db, _storage), - NullLogger.Instance, - TestData.StorageConfig(), - TestData.TrackConfig(), - TestData.UploadIntentConfig()); - - private void StorageHasObject() => - _storage.HeadObjectAsync(Arg.Any(), Arg.Any(), Arg.Any()) - .Returns(new ObjectMetaData("application/octet-stream", 500)); - - private static async Task<(UploadIntent picture, UploadIntent audio)> SeedIntentsAsync( - SqliteTestContext ctx, Guid userId) - { - var picture = TestData.UploadIntent(userId, UploadIntentPurpose.TrackPicture, objectName: "pic.png", key: $"temp/{userId}/pic.png"); - var audio = TestData.UploadIntent(userId, UploadIntentPurpose.TrackAudio, objectName: "song.mp3", key: $"temp/{userId}/song.mp3"); - ctx.Database.UploadIntents.AddRange(picture, audio); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - return (picture, audio); - } - - [Fact] - public async Task When_user_missing_returns_not_found() - { - await using var ctx = new SqliteTestContext(); - var sut = CreateSut(ctx.Database); - - var result = await sut.Handle( - new CreateTrackCommand(Guid.NewGuid(), "Song", Guid.NewGuid(), Guid.NewGuid()), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.NotFound); - } - - [Fact] - public async Task When_picture_intent_invalid_propagates_and_creates_no_track() - { - await using var ctx = new SqliteTestContext(); - var user = TestData.User(); - ctx.Database.Users.Add(user); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = CreateSut(ctx.Database); - var result = await sut.Handle( - new CreateTrackCommand(user.Id, "Song", Guid.NewGuid(), Guid.NewGuid()), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.NotFound); - (await ctx.NewContext().Tracks.CountAsync()).Should().Be(0); - await _eventBus.DidNotReceive().PublishAsync(Arg.Any(), Arg.Any()); - } - - [Fact] - public async Task When_audio_intent_invalid_propagates() - { - await using var ctx = new SqliteTestContext(); - var user = TestData.User(); - var picture = TestData.UploadIntent(user.Id, UploadIntentPurpose.TrackPicture, objectName: "pic.png", key: $"temp/{user.Id}/pic.png"); - ctx.Database.Users.Add(user); - ctx.Database.UploadIntents.Add(picture); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - StorageHasObject(); - - var sut = CreateSut(ctx.Database); - var result = await sut.Handle( - new CreateTrackCommand(user.Id, "Song", picture.Id, Guid.NewGuid()), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.NotFound); - } - - [Fact] - public async Task Happy_path_creates_track_publishes_event_and_normalizes_title() - { - await using var ctx = new SqliteTestContext(); - var user = TestData.User(); - ctx.Database.Users.Add(user); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - var (picture, audio) = await SeedIntentsAsync(ctx, user.Id); - StorageHasObject(); - - var sut = CreateSut(ctx.Database); - var result = await sut.Handle( - new CreateTrackCommand(user.Id, "My Song", picture.Id, audio.Id), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Created); - await _eventBus.Received(1).PublishAsync( - Arg.Is(e => - e.PictureIntentId == picture.Id && e.AudioIntentId == audio.Id), - Arg.Any()); - - var persisted = await ctx.NewContext().Tracks.SingleAsync(); - persisted.NormalizedTitle.Should().Be("MY SONG"); - persisted.OriginalPictureName.Should().Be("pic.png"); - persisted.OriginalAudioName.Should().Be("song.mp3"); - } - - [Fact] - public async Task When_publish_throws_returns_error_and_does_not_persist() - { - await using var ctx = new SqliteTestContext(); - var user = TestData.User(); - ctx.Database.Users.Add(user); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - var (picture, audio) = await SeedIntentsAsync(ctx, user.Id); - StorageHasObject(); - - _eventBus.PublishAsync(Arg.Any(), Arg.Any()) - .Returns(_ => throw new InvalidOperationException("bus down")); - - var sut = CreateSut(ctx.Database); - var result = await sut.Handle( - new CreateTrackCommand(user.Id, "My Song", picture.Id, audio.Id), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Error); - (await ctx.NewContext().Tracks.CountAsync()).Should().Be(0); - } -} diff --git a/Tests/Musify.Application.Tests/Handlers/CreateUserCommandHandlerTests.cs b/Tests/Musify.Application.Tests/Handlers/CreateUserCommandHandlerTests.cs deleted file mode 100644 index 789a6dc..0000000 --- a/Tests/Musify.Application.Tests/Handlers/CreateUserCommandHandlerTests.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Ardalis.Result; -using FluentAssertions; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging.Abstractions; -using Musify.Application.Tests.Infrastructure; -using Musify.Application.Users.Commands; -using Musify.Application.Users.Handlers; -using Xunit; - -namespace Musify.Application.Tests.Handlers; - -public sealed class CreateUserCommandHandlerTests -{ - [Fact] - public async Task When_user_already_exists_returns_conflict() - { - await using var ctx = new SqliteTestContext(); - var existing = TestData.User(); - ctx.Database.Users.Add(existing); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = new CreateUserCommandHandler(ctx.Database, NullLogger.Instance); - var result = await sut.Handle(new CreateUserCommand(existing.Id, "Bob", null, null), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Conflict); - } - - [Fact] - public async Task Happy_path_creates_user_and_persists() - { - await using var ctx = new SqliteTestContext(); - var id = Guid.NewGuid(); - - var sut = new CreateUserCommandHandler(ctx.Database, NullLogger.Instance); - var result = await sut.Handle(new CreateUserCommand(id, "Bob", "Robert", "Smith"), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Created); - result.Value.Name.Should().Be("Bob"); - - var persisted = await ctx.NewContext().Users.SingleAsync(u => u.Id == id); - persisted.NormalizedName.Should().Be("BOB"); - persisted.FirstName.Should().Be("Robert"); - } -} diff --git a/Tests/Musify.Application.Tests/Handlers/DeletePlayListCommandHandlerTests.cs b/Tests/Musify.Application.Tests/Handlers/DeletePlayListCommandHandlerTests.cs deleted file mode 100644 index 9b69142..0000000 --- a/Tests/Musify.Application.Tests/Handlers/DeletePlayListCommandHandlerTests.cs +++ /dev/null @@ -1,88 +0,0 @@ -using Ardalis.Result; -using FluentAssertions; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging.Abstractions; -using Musify.Application.Contracts; -using Musify.Application.Events; -using Musify.Application.PlayLists.Commands; -using Musify.Application.PlayLists.Handlers; -using Musify.Application.Tests.Infrastructure; -using Musify.Domain.ValueObjects; -using NSubstitute; -using Xunit; - -namespace Musify.Application.Tests.Handlers; - -public sealed class DeletePlayListCommandHandlerTests -{ - private readonly IEventBus _eventBus = Substitute.For(); - - private DeletePlayListCommandHandler CreateSut(TestDatabase db) => - new(db, _eventBus, NullLogger.Instance); - - [Fact] - public async Task When_playlist_missing_returns_not_found() - { - await using var ctx = new SqliteTestContext(); - var sut = CreateSut(ctx.Database); - - var result = await sut.Handle(new DeletePlayListCommand(Guid.NewGuid(), Guid.NewGuid()), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.NotFound); - } - - [Fact] - public async Task When_user_not_owner_returns_unauthorized() - { - await using var ctx = new SqliteTestContext(); - var playList = TestData.PlayList(Guid.NewGuid()); - ctx.Database.PlayLists.Add(playList); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = CreateSut(ctx.Database); - var result = await sut.Handle(new DeletePlayListCommand(Guid.NewGuid(), playList.Id), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Unauthorized); - } - - [Fact] - public async Task Happy_path_marks_removing_publishes_event_and_returns_no_content() - { - await using var ctx = new SqliteTestContext(); - var userId = Guid.NewGuid(); - var playList = TestData.PlayList(userId); - ctx.Database.PlayLists.Add(playList); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = CreateSut(ctx.Database); - var result = await sut.Handle(new DeletePlayListCommand(userId, playList.Id), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.NoContent); - await _eventBus.Received(1).PublishAsync( - Arg.Is(e => e.PlayListId == playList.Id && e.UserId == userId), - Arg.Any()); - - var persisted = await ctx.NewContext().PlayLists.SingleAsync(p => p.Id == playList.Id); - persisted.LifeCycleStatus.Should().Be(LifeCycleStatus.Removing); - } - - [Fact] - public async Task When_publish_throws_returns_error_and_does_not_persist() - { - await using var ctx = new SqliteTestContext(); - var userId = Guid.NewGuid(); - var playList = TestData.PlayList(userId); - ctx.Database.PlayLists.Add(playList); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - _eventBus.PublishAsync(Arg.Any(), Arg.Any()) - .Returns(_ => throw new InvalidOperationException("bus down")); - - var sut = CreateSut(ctx.Database); - var result = await sut.Handle(new DeletePlayListCommand(userId, playList.Id), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Error); - var persisted = await ctx.NewContext().PlayLists.SingleAsync(p => p.Id == playList.Id); - persisted.LifeCycleStatus.Should().Be(LifeCycleStatus.Active); - } -} diff --git a/Tests/Musify.Application.Tests/Handlers/DeleteTrackCommandHandlerTests.cs b/Tests/Musify.Application.Tests/Handlers/DeleteTrackCommandHandlerTests.cs deleted file mode 100644 index e81a555..0000000 --- a/Tests/Musify.Application.Tests/Handlers/DeleteTrackCommandHandlerTests.cs +++ /dev/null @@ -1,108 +0,0 @@ -using Ardalis.Result; -using FluentAssertions; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging.Abstractions; -using Musify.Application.Contracts; -using Musify.Application.Events; -using Musify.Application.Tests.Infrastructure; -using Musify.Application.Tracks.Commands; -using Musify.Application.Tracks.Handlers; -using Musify.Domain.Entities; -using Musify.Domain.ValueObjects; -using NSubstitute; -using Xunit; - -namespace Musify.Application.Tests.Handlers; - -public sealed class DeleteTrackCommandHandlerTests -{ - private readonly IEventBus _eventBus = Substitute.For(); - - private DeleteTrackCommandHandler CreateSut(TestDatabase db) => - new(db, _eventBus, NullLogger.Instance); - - private static async Task<(Guid trackId, Guid userId)> SeedOwnedTrackAsync( - SqliteTestContext ctx, ProcessingStatus audioStatus) - { - var userId = Guid.NewGuid(); - var track = TestData.Track(audioStatus: audioStatus); - ctx.Database.Tracks.Add(track); - ctx.Database.UserHasTracks.Add(new UserHasTrack { UserId = userId, TrackId = track.Id }); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - return (track.Id, userId); - } - - [Fact] - public async Task When_track_missing_returns_not_found() - { - await using var ctx = new SqliteTestContext(); - var sut = CreateSut(ctx.Database); - - var result = await sut.Handle(new DeleteTrackCommand(Guid.NewGuid(), Guid.NewGuid()), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.NotFound); - } - - [Fact] - public async Task When_user_not_owner_returns_unauthorized() - { - await using var ctx = new SqliteTestContext(); - var track = TestData.Track(); - ctx.Database.Tracks.Add(track); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = CreateSut(ctx.Database); - var result = await sut.Handle(new DeleteTrackCommand(Guid.NewGuid(), track.Id), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Unauthorized); - } - - [Theory] - [InlineData(ProcessingStatus.Pending)] - [InlineData(ProcessingStatus.Processing)] - public async Task When_audio_still_processing_returns_conflict(ProcessingStatus status) - { - await using var ctx = new SqliteTestContext(); - var (trackId, userId) = await SeedOwnedTrackAsync(ctx, status); - - var sut = CreateSut(ctx.Database); - var result = await sut.Handle(new DeleteTrackCommand(userId, trackId), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Conflict); - await _eventBus.DidNotReceive().PublishAsync(Arg.Any(), Arg.Any()); - } - - [Fact] - public async Task Happy_path_marks_removing_publishes_event_and_returns_no_content() - { - await using var ctx = new SqliteTestContext(); - var (trackId, userId) = await SeedOwnedTrackAsync(ctx, ProcessingStatus.Completed); - - var sut = CreateSut(ctx.Database); - var result = await sut.Handle(new DeleteTrackCommand(userId, trackId), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.NoContent); - await _eventBus.Received(1).PublishAsync( - Arg.Is(e => e.TrackId == trackId), Arg.Any()); - - var persisted = await ctx.NewContext().Tracks.SingleAsync(t => t.Id == trackId); - persisted.LifeCycleStatus.Should().Be(LifeCycleStatus.Removing); - } - - [Fact] - public async Task When_publish_throws_returns_error_and_does_not_persist() - { - await using var ctx = new SqliteTestContext(); - var (trackId, userId) = await SeedOwnedTrackAsync(ctx, ProcessingStatus.Completed); - - _eventBus.PublishAsync(Arg.Any(), Arg.Any()) - .Returns(_ => throw new InvalidOperationException("bus down")); - - var sut = CreateSut(ctx.Database); - var result = await sut.Handle(new DeleteTrackCommand(userId, trackId), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Error); - var persisted = await ctx.NewContext().Tracks.SingleAsync(t => t.Id == trackId); - persisted.LifeCycleStatus.Should().Be(LifeCycleStatus.Active); - } -} diff --git a/Tests/Musify.Application.Tests/Handlers/GetByIdQueryHandlerTests.cs b/Tests/Musify.Application.Tests/Handlers/GetByIdQueryHandlerTests.cs deleted file mode 100644 index 7c29582..0000000 --- a/Tests/Musify.Application.Tests/Handlers/GetByIdQueryHandlerTests.cs +++ /dev/null @@ -1,93 +0,0 @@ -using Ardalis.Result; -using FluentAssertions; -using Musify.Application.PlayLists.Handlers; -using Musify.Application.PlayLists.Queries; -using Musify.Application.Tests.Infrastructure; -using Musify.Application.Tracks.Handlers; -using Musify.Application.Tracks.Queries; -using Musify.Application.Users.Handlers; -using Musify.Application.Users.Queries; -using Xunit; - -namespace Musify.Application.Tests.Handlers; - -public sealed class GetByIdQueryHandlerTests -{ - [Fact] - public async Task GetTrackById_returns_track_when_found() - { - await using var ctx = new SqliteTestContext(); - var track = TestData.Track(title: "Found"); - ctx.Database.Tracks.Add(track); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = new GetTrackByIdQueryHandler(ctx.Database); - var result = await sut.Handle(new GetTrackByIdQuery(track.Id), CancellationToken.None); - - result.IsSuccess.Should().BeTrue(); - result.Value.Title.Should().Be("Found"); - } - - [Fact] - public async Task GetTrackById_returns_not_found_when_missing() - { - await using var ctx = new SqliteTestContext(); - var sut = new GetTrackByIdQueryHandler(ctx.Database); - - var result = await sut.Handle(new GetTrackByIdQuery(Guid.NewGuid()), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.NotFound); - } - - [Fact] - public async Task GetPlayListById_returns_playlist_when_found() - { - await using var ctx = new SqliteTestContext(); - var playList = TestData.PlayList(Guid.NewGuid(), name: "MyList"); - ctx.Database.PlayLists.Add(playList); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = new GetPlayListByIdQueryHandler(ctx.Database); - var result = await sut.Handle(new GetPlayListByIdQuery(playList.Id), CancellationToken.None); - - result.IsSuccess.Should().BeTrue(); - result.Value.Name.Should().Be("MyList"); - } - - [Fact] - public async Task GetPlayListById_returns_not_found_when_missing() - { - await using var ctx = new SqliteTestContext(); - var sut = new GetPlayListByIdQueryHandler(ctx.Database); - - var result = await sut.Handle(new GetPlayListByIdQuery(Guid.NewGuid()), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.NotFound); - } - - [Fact] - public async Task GetUserById_returns_user_when_found() - { - await using var ctx = new SqliteTestContext(); - var user = TestData.User(name: "Carol"); - ctx.Database.Users.Add(user); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = new GetUserByIdQueryHandler(ctx.Database); - var result = await sut.Handle(new GetUserByIdQuery(user.Id), CancellationToken.None); - - result.IsSuccess.Should().BeTrue(); - result.Value.Name.Should().Be("Carol"); - } - - [Fact] - public async Task GetUserById_returns_not_found_when_missing() - { - await using var ctx = new SqliteTestContext(); - var sut = new GetUserByIdQueryHandler(ctx.Database); - - var result = await sut.Handle(new GetUserByIdQuery(Guid.NewGuid()), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.NotFound); - } -} diff --git a/Tests/Musify.Application.Tests/Handlers/GetTracksByUserIdQueryHandlerTests.cs b/Tests/Musify.Application.Tests/Handlers/GetTracksByUserIdQueryHandlerTests.cs deleted file mode 100644 index 0352e47..0000000 --- a/Tests/Musify.Application.Tests/Handlers/GetTracksByUserIdQueryHandlerTests.cs +++ /dev/null @@ -1,83 +0,0 @@ -using FluentAssertions; -using Musify.Application.Tests.Infrastructure; -using Musify.Application.Tracks.Handlers; -using Musify.Application.Tracks.Queries; -using Musify.Domain.Entities; -using Xunit; - -namespace Musify.Application.Tests.Handlers; - -public sealed class GetTracksByUserIdQueryHandlerTests -{ - private static async Task SeedTrackForUserAsync(SqliteTestContext ctx, Guid userId, string title, DateTime created) - { - var track = TestData.Track(title: title); - track.CreatedDate = created; - ctx.Database.Tracks.Add(track); - ctx.Database.UserHasTracks.Add(new UserHasTrack { UserId = userId, TrackId = track.Id }); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - } - - [Fact] - public async Task Returns_only_tracks_owned_by_the_user_ordered_by_created_date() - { - await using var ctx = new SqliteTestContext(); - var userId = Guid.NewGuid(); - var otherUser = Guid.NewGuid(); - await SeedTrackForUserAsync(ctx, userId, "Beta", new DateTime(2026, 1, 2, 0, 0, 0, DateTimeKind.Utc)); - await SeedTrackForUserAsync(ctx, userId, "Alpha", new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc)); - await SeedTrackForUserAsync(ctx, otherUser, "Gamma", new DateTime(2026, 1, 3, 0, 0, 0, DateTimeKind.Utc)); - - var sut = new GetTracksByUserIdQueryHandler(ctx.Database); - var result = await sut.Handle(new GetTracksByUserIdQuery(userId), CancellationToken.None); - - result.IsSuccess.Should().BeTrue(); - result.Value.TotalItemCount.Should().Be(2); - result.Value.Items.Select(t => t.Title).Should().Equal("Alpha", "Beta"); - } - - [Fact] - public async Task Filters_by_normalized_name_contains() - { - await using var ctx = new SqliteTestContext(); - var userId = Guid.NewGuid(); - await SeedTrackForUserAsync(ctx, userId, "Summer Hit", DateTime.UtcNow); - await SeedTrackForUserAsync(ctx, userId, "Winter Song", DateTime.UtcNow); - - var sut = new GetTracksByUserIdQueryHandler(ctx.Database); - var result = await sut.Handle(new GetTracksByUserIdQuery(userId, Name: "summer"), CancellationToken.None); - - result.Value.TotalItemCount.Should().Be(1); - result.Value.Items.Single().Title.Should().Be("Summer Hit"); - } - - [Fact] - public async Task Normalizes_invalid_paging_arguments_to_defaults() - { - await using var ctx = new SqliteTestContext(); - var userId = Guid.NewGuid(); - for (var i = 0; i < 3; i++) - await SeedTrackForUserAsync(ctx, userId, $"Track {i}", DateTime.UtcNow.AddSeconds(i)); - - var sut = new GetTracksByUserIdQueryHandler(ctx.Database); - var result = await sut.Handle( - new GetTracksByUserIdQuery(userId, PageNumber: 0, PageSize: -5), CancellationToken.None); - - result.Value.PageNumber.Should().Be(1); - result.Value.PageSize.Should().Be(10); - result.Value.TotalItemCount.Should().Be(3); - } - - [Fact] - public async Task Returns_empty_page_when_user_has_no_tracks() - { - await using var ctx = new SqliteTestContext(); - - var sut = new GetTracksByUserIdQueryHandler(ctx.Database); - var result = await sut.Handle(new GetTracksByUserIdQuery(Guid.NewGuid()), CancellationToken.None); - - result.IsSuccess.Should().BeTrue(); - result.Value.TotalItemCount.Should().Be(0); - result.Value.Items.Should().BeEmpty(); - } -} diff --git a/Tests/Musify.Application.Tests/Handlers/ListQueryHandlerTests.cs b/Tests/Musify.Application.Tests/Handlers/ListQueryHandlerTests.cs deleted file mode 100644 index 326a9fe..0000000 --- a/Tests/Musify.Application.Tests/Handlers/ListQueryHandlerTests.cs +++ /dev/null @@ -1,135 +0,0 @@ -using FluentAssertions; -using Musify.Application.PlayLists.Handlers; -using Musify.Application.PlayLists.Queries; -using Musify.Application.Tests.Infrastructure; -using Musify.Application.Tracks.Handlers; -using Musify.Application.Tracks.Queries; -using Musify.Application.Users.Handlers; -using Musify.Application.Users.Queries; -using Musify.Domain.Entities; -using Xunit; - -namespace Musify.Application.Tests.Handlers; - -public sealed class ListQueryHandlerTests -{ - [Fact] - public async Task GetTracks_paginates_across_all_users() - { - await using var ctx = new SqliteTestContext(); - for (var i = 0; i < 15; i++) - { - var track = TestData.Track(title: $"Track {i:D2}"); - ctx.Database.Tracks.Add(track); - ctx.Database.UserHasTracks.Add(new UserHasTrack { UserId = Guid.NewGuid(), TrackId = track.Id }); - } - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = new GetTracksQueryHandler(ctx.Database); - var result = await sut.Handle(new GetTracksQuery(PageNumber: 2, PageSize: 10), CancellationToken.None); - - result.IsSuccess.Should().BeTrue(); - result.Value.TotalItemCount.Should().Be(15); - result.Value.PageNumber.Should().Be(2); - result.Value.Items.Should().HaveCount(5); - result.Value.HasPreviousPage.Should().BeTrue(); - result.Value.HasNextPage.Should().BeFalse(); - } - - [Fact] - public async Task GetTracks_normalizes_invalid_paging_to_defaults() - { - await using var ctx = new SqliteTestContext(); - var track = TestData.Track(); - ctx.Database.Tracks.Add(track); - ctx.Database.UserHasTracks.Add(new UserHasTrack { UserId = Guid.NewGuid(), TrackId = track.Id }); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = new GetTracksQueryHandler(ctx.Database); - var result = await sut.Handle(new GetTracksQuery(PageNumber: 0, PageSize: -3), CancellationToken.None); - - result.Value.PageNumber.Should().Be(1); - result.Value.PageSize.Should().Be(10); - result.Value.TotalItemCount.Should().Be(1); - } - - [Fact] - public async Task GetPlayLists_returns_all_playlists_paginated() - { - await using var ctx = new SqliteTestContext(); - for (var i = 0; i < 3; i++) - ctx.Database.PlayLists.Add(TestData.PlayList(Guid.NewGuid(), name: $"PL {i}")); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = new GetPlayListsQueryHandler(ctx.Database); - var result = await sut.Handle(new GetPlayListsQuery(), CancellationToken.None); - - result.IsSuccess.Should().BeTrue(); - result.Value.TotalItemCount.Should().Be(3); - result.Value.Items.Should().HaveCount(3); - } - - [Fact] - public async Task GetPlayListsByUserId_filters_by_user_and_name() - { - await using var ctx = new SqliteTestContext(); - var userId = Guid.NewGuid(); - ctx.Database.PlayLists.Add(TestData.PlayList(userId, name: "Summer Mix")); - ctx.Database.PlayLists.Add(TestData.PlayList(userId, name: "Winter Mix")); - ctx.Database.PlayLists.Add(TestData.PlayList(Guid.NewGuid(), name: "Summer Other")); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = new GetPlayListsByUserIdQueryHandler(ctx.Database); - - var all = await sut.Handle(new GetPlayListsByUserIdQuery(userId), CancellationToken.None); - all.Value.TotalItemCount.Should().Be(2); - - var filtered = await sut.Handle(new GetPlayListsByUserIdQuery(userId, Name: "summer"), CancellationToken.None); - filtered.Value.TotalItemCount.Should().Be(1); - filtered.Value.Items.Single().Name.Should().Be("Summer Mix"); - } - - [Fact] - public async Task GetPlayListsByUserId_returns_empty_for_unknown_user() - { - await using var ctx = new SqliteTestContext(); - var sut = new GetPlayListsByUserIdQueryHandler(ctx.Database); - - var result = await sut.Handle(new GetPlayListsByUserIdQuery(Guid.NewGuid()), CancellationToken.None); - - result.IsSuccess.Should().BeTrue(); - result.Value.TotalItemCount.Should().Be(0); - } - - [Fact] - public async Task GetUsers_filters_by_username_search_case_insensitively() - { - await using var ctx = new SqliteTestContext(); - ctx.Database.Users.Add(TestData.User(name: "Alice")); - ctx.Database.Users.Add(TestData.User(name: "Bob")); - ctx.Database.Users.Add(TestData.User(name: "Alicia")); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = new GetUsersQueryHandler(ctx.Database); - var result = await sut.Handle(new GetUsersQuery(1, 10, UsernameSearch: "ali"), CancellationToken.None); - - result.IsSuccess.Should().BeTrue(); - result.Value.TotalItemCount.Should().Be(2); - result.Value.Items.Select(u => u.Name).Should().BeEquivalentTo("Alice", "Alicia"); - } - - [Fact] - public async Task GetUsers_normalizes_invalid_paging_to_defaults() - { - await using var ctx = new SqliteTestContext(); - ctx.Database.Users.Add(TestData.User(name: "Solo")); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = new GetUsersQueryHandler(ctx.Database); - var result = await sut.Handle(new GetUsersQuery(0, -1), CancellationToken.None); - - result.Value.PageNumber.Should().Be(1); - result.Value.PageSize.Should().Be(10); - result.Value.TotalItemCount.Should().Be(1); - } -} diff --git a/Tests/Musify.Application.Tests/Handlers/RequestPlayListPictureUploadCommandHandlerTests.cs b/Tests/Musify.Application.Tests/Handlers/RequestPlayListPictureUploadCommandHandlerTests.cs deleted file mode 100644 index 2268920..0000000 --- a/Tests/Musify.Application.Tests/Handlers/RequestPlayListPictureUploadCommandHandlerTests.cs +++ /dev/null @@ -1,129 +0,0 @@ -using Ardalis.Result; -using FluentAssertions; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging.Abstractions; -using Musify.Application.Configuration; -using Musify.Application.Contracts; -using Musify.Application.PlayLists.Commands; -using Musify.Application.PlayLists.Handlers; -using Musify.Application.Services; -using Musify.Application.Tests.Infrastructure; -using Musify.Domain.ValueObjects; -using NSubstitute; -using Xunit; - -namespace Musify.Application.Tests.Handlers; - -public sealed class RequestPlayListPictureUploadCommandHandlerTests -{ - private readonly IStorageService _storage = Substitute.For(); - - private RequestPlayListPictureUploadCommandHandler CreateSut(TestDatabase db, UploadIntentConfiguration? config = null) => - new( - db, - new UploadIntentValidator(db, _storage), - _storage, - NullLogger.Instance, - TestData.StorageConfig(), - TestData.PlayListConfig(), - config ?? TestData.UploadIntentConfig()); - - private static RequestPlayListPictureUploadCommand Command(Guid userId, long? size = null) => - new(userId, ".WEBP", "image/webp", size); - - [Fact] - public async Task When_user_missing_returns_not_found() - { - await using var ctx = new SqliteTestContext(); - var sut = CreateSut(ctx.Database); - - var result = await sut.Handle(Command(Guid.NewGuid()), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.NotFound); - } - - [Fact] - public async Task Happy_path_creates_single_intent_and_returns_url() - { - await using var ctx = new SqliteTestContext(); - var user = TestData.User(); - ctx.Database.Users.Add(user); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - _storage.GetUploadUrlAsync( - Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) - .Returns("https://signed.example/upload"); - - var sut = CreateSut(ctx.Database); - var result = await sut.Handle(Command(user.Id), CancellationToken.None); - - result.IsSuccess.Should().BeTrue(); - result.Value.UploadUrl.Should().Be("https://signed.example/upload"); - - var intents = await ctx.NewContext().UploadIntents.Where(i => i.UserId == user.Id).ToListAsync(); - intents.Should().ContainSingle(); - intents[0].Purpose.Should().Be(UploadIntentPurpose.PlayListPicture); - intents[0].ObjectName.Should().EndWith(".webp"); - } - - [Fact] - public async Task When_quota_exceeded_returns_invalid_and_creates_no_intent() - { - await using var ctx = new SqliteTestContext(); - var user = TestData.User(); - ctx.Database.Users.Add(user); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - _storage.GetUploadUrlAsync( - Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) - .Returns("https://signed.example/upload"); - - var config = TestData.UploadIntentConfig(); - config.MaxActiveUploadIntentsPerUser = 0; - - var sut = CreateSut(ctx.Database, config); - var result = await sut.Handle(Command(user.Id), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Invalid); - (await ctx.NewContext().UploadIntents.CountAsync()).Should().Be(0); - } - - [Fact] - public async Task Uses_default_picture_size_when_not_provided() - { - await using var ctx = new SqliteTestContext(); - var user = TestData.User(); - ctx.Database.Users.Add(user); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - _storage.GetUploadUrlAsync( - Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) - .Returns("https://signed.example/upload"); - - var config = TestData.UploadIntentConfig(); - var sut = CreateSut(ctx.Database, config); - await sut.Handle(Command(user.Id), CancellationToken.None); - - var intent = await ctx.NewContext().UploadIntents.SingleAsync(i => i.UserId == user.Id); - intent.ExpectedSizeBytes.Should().Be(config.DefaultExpectedPictureSizeBytes); - } - - [Fact] - public async Task When_storage_throws_returns_error() - { - await using var ctx = new SqliteTestContext(); - var user = TestData.User(); - ctx.Database.Users.Add(user); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - _storage.GetUploadUrlAsync( - Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) - .Returns(_ => throw new InvalidOperationException("s3 down")); - - var sut = CreateSut(ctx.Database); - var result = await sut.Handle(Command(user.Id), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Error); - (await ctx.NewContext().UploadIntents.CountAsync()).Should().Be(0); - } -} diff --git a/Tests/Musify.Application.Tests/Handlers/RequestTrackUploadUrlsCommandHandlerTests.cs b/Tests/Musify.Application.Tests/Handlers/RequestTrackUploadUrlsCommandHandlerTests.cs deleted file mode 100644 index c116dd4..0000000 --- a/Tests/Musify.Application.Tests/Handlers/RequestTrackUploadUrlsCommandHandlerTests.cs +++ /dev/null @@ -1,134 +0,0 @@ -using Ardalis.Result; -using FluentAssertions; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging.Abstractions; -using Musify.Application.Configuration; -using Musify.Application.Contracts; -using Musify.Application.Services; -using Musify.Application.Tests.Infrastructure; -using Musify.Application.Tracks.Commands; -using Musify.Application.Tracks.Handlers; -using Musify.Domain.ValueObjects; -using NSubstitute; -using Xunit; - -namespace Musify.Application.Tests.Handlers; - -public sealed class RequestTrackUploadUrlsCommandHandlerTests -{ - private readonly IStorageService _storage = Substitute.For(); - - private RequestTrackUploadUrlsCommandHandler CreateSut(TestDatabase db, UploadIntentConfiguration? config = null) => - new( - db, - new UploadIntentValidator(db, _storage), - _storage, - NullLogger.Instance, - TestData.StorageConfig(), - TestData.TrackConfig(), - config ?? TestData.UploadIntentConfig()); - - private static RequestTrackUploadUrlsCommand Command(Guid userId, long? pic = null, long? audio = null) => - new(userId, ".PNG", "image/png", "mp3", "audio/mpeg", pic, audio); - - [Fact] - public async Task When_user_missing_returns_not_found() - { - await using var ctx = new SqliteTestContext(); - var sut = CreateSut(ctx.Database); - - var result = await sut.Handle(Command(Guid.NewGuid()), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.NotFound); - } - - [Fact] - public async Task Happy_path_creates_two_intents_and_returns_urls() - { - await using var ctx = new SqliteTestContext(); - var user = TestData.User(); - ctx.Database.Users.Add(user); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - _storage.GetUploadUrlAsync( - Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) - .Returns("https://signed.example/upload"); - - var sut = CreateSut(ctx.Database); - var result = await sut.Handle(Command(user.Id), CancellationToken.None); - - result.IsSuccess.Should().BeTrue(); - result.Value.PictureUploadUrl.Should().Be("https://signed.example/upload"); - - var intents = await ctx.NewContext().UploadIntents.Where(i => i.UserId == user.Id).ToListAsync(); - intents.Should().HaveCount(2); - intents.Select(i => i.Purpose).Should().BeEquivalentTo( - new[] { UploadIntentPurpose.TrackPicture, UploadIntentPurpose.TrackAudio }); - intents.Should().Contain(i => i.ObjectName.EndsWith(".png")); - intents.Should().Contain(i => i.ObjectName.EndsWith(".mp3")); - } - - [Fact] - public async Task When_quota_exceeded_returns_invalid_and_creates_no_intents() - { - await using var ctx = new SqliteTestContext(); - var user = TestData.User(); - ctx.Database.Users.Add(user); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - _storage.GetUploadUrlAsync( - Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) - .Returns("https://signed.example/upload"); - - var config = TestData.UploadIntentConfig(); - config.MaxActiveUploadIntentsPerUser = 1; - - var sut = CreateSut(ctx.Database, config); - var result = await sut.Handle(Command(user.Id), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Invalid); - (await ctx.NewContext().UploadIntents.CountAsync()).Should().Be(0); - } - - [Fact] - public async Task Uses_default_sizes_when_not_provided() - { - await using var ctx = new SqliteTestContext(); - var user = TestData.User(); - ctx.Database.Users.Add(user); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - _storage.GetUploadUrlAsync( - Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) - .Returns("https://signed.example/upload"); - - var config = TestData.UploadIntentConfig(); - var sut = CreateSut(ctx.Database, config); - await sut.Handle(Command(user.Id), CancellationToken.None); - - var intents = await ctx.NewContext().UploadIntents.Where(i => i.UserId == user.Id).ToListAsync(); - intents.Single(i => i.Purpose == UploadIntentPurpose.TrackPicture) - .ExpectedSizeBytes.Should().Be(config.DefaultExpectedPictureSizeBytes); - intents.Single(i => i.Purpose == UploadIntentPurpose.TrackAudio) - .ExpectedSizeBytes.Should().Be(config.DefaultExpectedAudioSizeBytes); - } - - [Fact] - public async Task When_storage_throws_returns_error() - { - await using var ctx = new SqliteTestContext(); - var user = TestData.User(); - ctx.Database.Users.Add(user); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - _storage.GetUploadUrlAsync( - Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) - .Returns(_ => throw new InvalidOperationException("s3 down")); - - var sut = CreateSut(ctx.Database); - var result = await sut.Handle(Command(user.Id), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Error); - (await ctx.NewContext().UploadIntents.CountAsync()).Should().Be(0); - } -} diff --git a/Tests/Musify.Application.Tests/Handlers/UpdatePlayListCommandHandlerTests.cs b/Tests/Musify.Application.Tests/Handlers/UpdatePlayListCommandHandlerTests.cs deleted file mode 100644 index 52957bc..0000000 --- a/Tests/Musify.Application.Tests/Handlers/UpdatePlayListCommandHandlerTests.cs +++ /dev/null @@ -1,144 +0,0 @@ -using Ardalis.Result; -using FluentAssertions; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging.Abstractions; -using Musify.Application.Contracts; -using Musify.Application.Events; -using Musify.Application.PlayLists.Commands; -using Musify.Application.PlayLists.Handlers; -using Musify.Application.Services; -using Musify.Application.Tests.Infrastructure; -using Musify.Domain.ValueObjects; -using NSubstitute; -using Xunit; - -namespace Musify.Application.Tests.Handlers; - -public sealed class UpdatePlayListCommandHandlerTests -{ - private readonly IEventBus _eventBus = Substitute.For(); - private readonly IStorageService _storage = Substitute.For(); - - private UpdatePlayListCommandHandler CreateSut(TestDatabase db) => - new( - _eventBus, - db, - new UploadIntentValidator(db, _storage), - NullLogger.Instance, - TestData.StorageConfig(), - TestData.PlayListConfig(), - TestData.UploadIntentConfig()); - - [Fact] - public async Task When_playlist_missing_returns_not_found() - { - await using var ctx = new SqliteTestContext(); - var sut = CreateSut(ctx.Database); - - var result = await sut.Handle( - new UpdatePlayListCommand(Guid.NewGuid(), Guid.NewGuid(), "n", null, null), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.NotFound); - } - - [Fact] - public async Task When_user_not_owner_returns_unauthorized() - { - await using var ctx = new SqliteTestContext(); - var playList = TestData.PlayList(Guid.NewGuid()); - ctx.Database.PlayLists.Add(playList); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = CreateSut(ctx.Database); - var result = await sut.Handle( - new UpdatePlayListCommand(Guid.NewGuid(), playList.Id, "n", null, null), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Unauthorized); - } - - [Fact] - public async Task Updates_name_and_description_without_publishing() - { - await using var ctx = new SqliteTestContext(); - var userId = Guid.NewGuid(); - var playList = TestData.PlayList(userId, name: "Old"); - ctx.Database.PlayLists.Add(playList); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = CreateSut(ctx.Database); - var result = await sut.Handle( - new UpdatePlayListCommand(userId, playList.Id, " New Name ", "new desc", null), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Ok); - await _eventBus.DidNotReceive().PublishAsync(Arg.Any(), Arg.Any()); - - var persisted = await ctx.NewContext().PlayLists.SingleAsync(p => p.Id == playList.Id); - persisted.Name.Should().Be(" New Name "); - persisted.NormalizedName.Should().Be("NEW NAME"); - persisted.Description.Should().Be("new desc"); - } - - [Fact] - public async Task When_only_description_provided_name_is_unchanged() - { - await using var ctx = new SqliteTestContext(); - var userId = Guid.NewGuid(); - var playList = TestData.PlayList(userId, name: "Keep"); - ctx.Database.PlayLists.Add(playList); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = CreateSut(ctx.Database); - var result = await sut.Handle( - new UpdatePlayListCommand(userId, playList.Id, null, "only desc", null), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Ok); - var persisted = await ctx.NewContext().PlayLists.SingleAsync(p => p.Id == playList.Id); - persisted.Name.Should().Be("Keep"); - persisted.Description.Should().Be("only desc"); - } - - [Fact] - public async Task With_valid_new_picture_intent_publishes_source_event_and_updates_picture_name() - { - await using var ctx = new SqliteTestContext(); - var userId = Guid.NewGuid(); - var playList = TestData.PlayList(userId); - var intent = TestData.UploadIntent(userId, UploadIntentPurpose.PlayListPicture, objectName: "newcover.webp"); - ctx.Database.PlayLists.Add(playList); - ctx.Database.UploadIntents.Add(intent); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - _storage.HeadObjectAsync(intent.Bucket, intent.Key, Arg.Any()) - .Returns(new ObjectMetaData("image/webp", 500)); - - var sut = CreateSut(ctx.Database); - var result = await sut.Handle( - new UpdatePlayListCommand(userId, playList.Id, null, null, intent.Id), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Ok); - await _eventBus.Received(1).PublishAsync( - Arg.Is(e => e.PlayListId == playList.Id && e.PictureIntentId == intent.Id), - Arg.Any()); - - var persisted = await ctx.NewContext().PlayLists.SingleAsync(p => p.Id == playList.Id); - persisted.OriginalPictureName.Should().Be("newcover.webp"); - } - - [Fact] - public async Task With_invalid_new_picture_intent_propagates_error() - { - await using var ctx = new SqliteTestContext(); - var userId = Guid.NewGuid(); - var playList = TestData.PlayList(userId); - var intent = TestData.UploadIntent(userId, UploadIntentPurpose.PlayListPicture, status: UploadIntentStatus.Consumed); - ctx.Database.PlayLists.Add(playList); - ctx.Database.UploadIntents.Add(intent); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = CreateSut(ctx.Database); - var result = await sut.Handle( - new UpdatePlayListCommand(userId, playList.Id, null, null, intent.Id), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Conflict); - } -} diff --git a/Tests/Musify.Application.Tests/Infrastructure/SchemaSmokeTests.cs b/Tests/Musify.Application.Tests/Infrastructure/SchemaSmokeTests.cs deleted file mode 100644 index 5544404..0000000 --- a/Tests/Musify.Application.Tests/Infrastructure/SchemaSmokeTests.cs +++ /dev/null @@ -1,42 +0,0 @@ -using FluentAssertions; -using Microsoft.EntityFrameworkCore; -using Xunit; - -namespace Musify.Application.Tests.Infrastructure; - -public sealed class SchemaSmokeTests -{ - [Fact] - public async Task Schema_creates_and_persists_all_core_entities() - { - await using var ctx = new SqliteTestContext(); - - var user = TestData.User(); - ctx.Database.Users.Add(user); - ctx.Database.UploadIntents.Add(TestData.UploadIntent(user.Id)); - ctx.Database.PlayLists.Add(TestData.PlayList(user.Id)); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var verify = ctx.NewContext(); - (await verify.Users.CountAsync()).Should().Be(1); - (await verify.UploadIntents.CountAsync()).Should().Be(1); - (await verify.PlayLists.CountAsync()).Should().Be(1); - } - - [Fact] - public async Task Transactions_are_supported_under_sqlite() - { - await using var ctx = new SqliteTestContext(); - var user = TestData.User(); - - await using (var tx = await ctx.Database.BeginTransactionAsync( - System.Data.IsolationLevel.Serializable, CancellationToken.None)) - { - ctx.Database.Users.Add(user); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - await tx.CommitAsync(CancellationToken.None); - } - - (await ctx.NewContext().Users.CountAsync()).Should().Be(1); - } -} diff --git a/Tests/Musify.Application.Tests/Infrastructure/SqliteTestContext.cs b/Tests/Musify.Application.Tests/Infrastructure/SqliteTestContext.cs deleted file mode 100644 index ced7b5b..0000000 --- a/Tests/Musify.Application.Tests/Infrastructure/SqliteTestContext.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Microsoft.Data.Sqlite; -using Microsoft.EntityFrameworkCore; - -namespace Musify.Application.Tests.Infrastructure; - -public sealed class SqliteTestContext : IAsyncDisposable, IDisposable -{ - private readonly SqliteConnection _connection; - - public TestDatabase Database { get; } - - public SqliteTestContext() - { - _connection = new SqliteConnection("DataSource=:memory:;Foreign Keys=False"); - _connection.Open(); - - var options = new DbContextOptionsBuilder() - .UseSqlite(_connection) - .Options; - - Database = new TestDatabase(options); - Database.Database.EnsureCreated(); - } - - public TestDatabase NewContext() - { - var options = new DbContextOptionsBuilder() - .UseSqlite(_connection) - .Options; - return new TestDatabase(options); - } - - public void Dispose() - { - Database.Dispose(); - _connection.Dispose(); - } - - public async ValueTask DisposeAsync() - { - await Database.DisposeAsync(); - await _connection.DisposeAsync(); - } -} diff --git a/Tests/Musify.Application.Tests/Infrastructure/TestData.cs b/Tests/Musify.Application.Tests/Infrastructure/TestData.cs deleted file mode 100644 index 2e8ef62..0000000 --- a/Tests/Musify.Application.Tests/Infrastructure/TestData.cs +++ /dev/null @@ -1,75 +0,0 @@ -using Musify.Application.Configuration; -using Musify.Domain.Entities; -using Musify.Domain.ValueObjects; - -namespace Musify.Application.Tests.Infrastructure; - -public static class TestData -{ - public const string Bucket = "musify-test"; - - public static ApplicationStorageConfiguration StorageConfig() => new() { Bucket = Bucket }; - - public static UploadIntentConfiguration UploadIntentConfig() => new(); - - public static TrackConfiguration TrackConfig() => new(); - - public static PlayListConfiguration PlayListConfig() => new(); - - public static User User(Guid? id = null, string name = "Alice") => new() - { - Id = id ?? Guid.NewGuid(), - Name = name, - NormalizedName = name.ToUpperInvariant(), - }; - - public static UploadIntent UploadIntent( - Guid userId, - UploadIntentPurpose purpose = UploadIntentPurpose.TrackPicture, - UploadIntentStatus status = UploadIntentStatus.Issued, - DateTime? expiresAt = null, - long? expectedSizeBytes = null, - string objectName = "object.webp", - string? key = null) => new() - { - UserId = userId, - Bucket = Bucket, - Key = key ?? $"temp/{userId}/Tracks/{objectName}", - ObjectName = objectName, - ContentType = "image/webp", - ExpectedSizeBytes = expectedSizeBytes, - Purpose = purpose, - Status = status, - ExpiresAt = expiresAt ?? DateTime.UtcNow.AddMinutes(5), - }; - - public static Track Track( - string title = "Song", - ProcessingStatus audioStatus = ProcessingStatus.Completed, - LifeCycleStatus lifeCycle = LifeCycleStatus.Active) => new() - { - Title = title, - NormalizedTitle = title.ToUpperInvariant(), - OriginalPictureName = "pic.webp", - OriginalAudioName = "audio.mp3", - AudioTranscodeProcessingStatus = audioStatus, - PicturesProcessingStatus = ProcessingStatus.Completed, - LifeCycleStatus = lifeCycle, - }; - - public static PlayList PlayList( - Guid userId, - string name = "My PlayList", - LifeCycleStatus lifeCycle = LifeCycleStatus.Active) => new() - { - UserId = userId, - Name = name, - NormalizedName = name.ToUpperInvariant(), - Description = "desc", - OriginalPictureName = "orig.webp", - SmallPictureName = "small.webp", - MediumPictureName = "medium.webp", - LargePictureName = "large.webp", - LifeCycleStatus = lifeCycle, - }; -} diff --git a/Tests/Musify.Application.Tests/Infrastructure/TestDatabase.cs b/Tests/Musify.Application.Tests/Infrastructure/TestDatabase.cs deleted file mode 100644 index 0d70d13..0000000 --- a/Tests/Musify.Application.Tests/Infrastructure/TestDatabase.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Storage; -using Musify.Domain.Entities; -using AppIDatabase = Musify.Application.Contracts.IDatabase; -using IDatabaseTransaction = Musify.Application.Contracts.IDatabaseTransaction; - -namespace Musify.Application.Tests.Infrastructure; - -public sealed class TestDatabase(DbContextOptions options) : DbContext(options), AppIDatabase -{ - public DbSet Users => Set(); - - public DbSet Tracks => Set(); - - public DbSet PlayLists => Set(); - - public DbSet UserHasTracks => Set(); - - public DbSet PlayListHasTracks => Set(); - - public DbSet Uploads => Set(); - - public DbSet UploadIntents => Set(); - - public async Task BeginTransactionAsync( - System.Data.IsolationLevel isolationLevel, CancellationToken cancellationToken) - { - var transaction = await Database.BeginTransactionAsync(isolationLevel, cancellationToken); - return new TestDatabaseTransaction(transaction); - } - - private sealed class TestDatabaseTransaction(IDbContextTransaction inner) : IDatabaseTransaction - { - public Task CommitAsync(CancellationToken cancellationToken = default) => - inner.CommitAsync(cancellationToken); - - public ValueTask DisposeAsync() => inner.DisposeAsync(); - } -} diff --git a/Tests/Musify.Application.Tests/Musify.Application.Tests.csproj b/Tests/Musify.Application.Tests/Musify.Application.Tests.csproj deleted file mode 100644 index 67ef5a4..0000000 --- a/Tests/Musify.Application.Tests/Musify.Application.Tests.csproj +++ /dev/null @@ -1,29 +0,0 @@ - - - - net10.0 - enable - enable - false - Musify.Application.Tests - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - diff --git a/Tests/Musify.Application.Tests/Pagination/PaginatedResponseTests.cs b/Tests/Musify.Application.Tests/Pagination/PaginatedResponseTests.cs deleted file mode 100644 index 4cfc561..0000000 --- a/Tests/Musify.Application.Tests/Pagination/PaginatedResponseTests.cs +++ /dev/null @@ -1,38 +0,0 @@ -using FluentAssertions; -using Musify.Application.Pagination; -using X.PagedList; -using Xunit; - -namespace Musify.Application.Tests.Pagination; - -public sealed class PaginatedResponseTests -{ - [Fact] - public void FromPagedList_maps_all_fields_for_a_middle_page() - { - var source = Enumerable.Range(1, 25).ToList(); - var paged = new PagedList(source, pageNumber: 2, pageSize: 10); - - var response = PaginatedResponse.FromPagedList(paged); - - response.PageNumber.Should().Be(2); - response.PageSize.Should().Be(10); - response.PageCount.Should().Be(3); - response.TotalItemCount.Should().Be(25); - response.HasNextPage.Should().BeTrue(); - response.HasPreviousPage.Should().BeTrue(); - response.Items.Should().Equal(11, 12, 13, 14, 15, 16, 17, 18, 19, 20); - } - - [Fact] - public void FromPagedList_first_page_has_no_previous_page() - { - var paged = new PagedList(Enumerable.Range(1, 5).ToList(), pageNumber: 1, pageSize: 10); - - var response = PaginatedResponse.FromPagedList(paged); - - response.HasPreviousPage.Should().BeFalse(); - response.HasNextPage.Should().BeFalse(); - response.TotalItemCount.Should().Be(5); - } -} diff --git a/Tests/Musify.Application.Tests/Services/UploadIntentValidatorTests.cs b/Tests/Musify.Application.Tests/Services/UploadIntentValidatorTests.cs deleted file mode 100644 index 791d888..0000000 --- a/Tests/Musify.Application.Tests/Services/UploadIntentValidatorTests.cs +++ /dev/null @@ -1,256 +0,0 @@ -using Ardalis.Result; -using FluentAssertions; -using Musify.Application.Contracts; -using Musify.Application.Services; -using Musify.Application.Tests.Infrastructure; -using Musify.Domain.ValueObjects; -using NSubstitute; -using Xunit; - -namespace Musify.Application.Tests.Services; - -public sealed class UploadIntentValidatorTests -{ - private readonly IStorageService _storage = Substitute.For(); - - private UploadIntentValidator CreateSut(TestDatabase database) => new(database, _storage); - - [Fact] - public async Task CheckQuota_under_limits_returns_success() - { - await using var ctx = new SqliteTestContext(); - var userId = Guid.NewGuid(); - var config = TestData.UploadIntentConfig(); - var sut = CreateSut(ctx.Database); - - var result = await sut.CheckQuotaAsync(config, userId, requiredBytes: 1_000, requiredIntentCount: 1, CancellationToken.None); - - result.IsSuccess.Should().BeTrue(); - } - - [Fact] - public async Task CheckQuota_when_intent_count_would_exceed_max_returns_invalid() - { - await using var ctx = new SqliteTestContext(); - var userId = Guid.NewGuid(); - var config = TestData.UploadIntentConfig(); - config.MaxActiveUploadIntentsPerUser = 2; - - ctx.Database.UploadIntents.Add(TestData.UploadIntent(userId, expectedSizeBytes: 1)); - ctx.Database.UploadIntents.Add(TestData.UploadIntent(userId, expectedSizeBytes: 1)); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = CreateSut(ctx.Database); - var result = await sut.CheckQuotaAsync(config, userId, requiredBytes: 1, requiredIntentCount: 1, CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Invalid); - } - - [Fact] - public async Task CheckQuota_exactly_at_intent_limit_is_allowed() - { - await using var ctx = new SqliteTestContext(); - var userId = Guid.NewGuid(); - var config = TestData.UploadIntentConfig(); - config.MaxActiveUploadIntentsPerUser = 2; - - ctx.Database.UploadIntents.Add(TestData.UploadIntent(userId, expectedSizeBytes: 1)); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = CreateSut(ctx.Database); - var result = await sut.CheckQuotaAsync(config, userId, requiredBytes: 1, requiredIntentCount: 1, CancellationToken.None); - - result.IsSuccess.Should().BeTrue(); - } - - [Fact] - public async Task CheckQuota_when_byte_quota_exceeded_returns_invalid() - { - await using var ctx = new SqliteTestContext(); - var userId = Guid.NewGuid(); - var config = TestData.UploadIntentConfig(); - config.MaxActiveUploadBytesPerUser = 1_000; - - ctx.Database.UploadIntents.Add(TestData.UploadIntent(userId, expectedSizeBytes: 800)); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = CreateSut(ctx.Database); - var result = await sut.CheckQuotaAsync(config, userId, requiredBytes: 300, requiredIntentCount: 1, CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Invalid); - } - - [Fact] - public async Task CheckQuota_uses_default_picture_size_for_intents_without_expected_size() - { - await using var ctx = new SqliteTestContext(); - var userId = Guid.NewGuid(); - var config = TestData.UploadIntentConfig(); - config.MaxActiveUploadBytesPerUser = 1_000; - config.DefaultExpectedPictureSizeBytes = 900; - - ctx.Database.UploadIntents.Add(TestData.UploadIntent(userId, expectedSizeBytes: null)); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = CreateSut(ctx.Database); - var result = await sut.CheckQuotaAsync(config, userId, requiredBytes: 200, requiredIntentCount: 1, CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Invalid); - } - - [Fact] - public async Task CheckQuota_ignores_other_users_and_non_issued_intents() - { - await using var ctx = new SqliteTestContext(); - var userId = Guid.NewGuid(); - var otherUser = Guid.NewGuid(); - var config = TestData.UploadIntentConfig(); - config.MaxActiveUploadIntentsPerUser = 2; - config.MaxActiveUploadBytesPerUser = 1_000; - - ctx.Database.UploadIntents.Add(TestData.UploadIntent(otherUser, expectedSizeBytes: 5_000)); - ctx.Database.UploadIntents.Add(TestData.UploadIntent(userId, status: UploadIntentStatus.Consumed, expectedSizeBytes: 5_000)); - ctx.Database.UploadIntents.Add(TestData.UploadIntent(userId, status: UploadIntentStatus.Expired, expectedSizeBytes: 5_000)); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = CreateSut(ctx.Database); - var result = await sut.CheckQuotaAsync(config, userId, requiredBytes: 100, requiredIntentCount: 1, CancellationToken.None); - - result.IsSuccess.Should().BeTrue(); - } - - [Fact] - public async Task ValidateAndLoad_when_intent_missing_returns_not_found() - { - await using var ctx = new SqliteTestContext(); - var sut = CreateSut(ctx.Database); - - var result = await sut.ValidateAndLoadAsync( - TestData.UploadIntentConfig(), Guid.NewGuid(), Guid.NewGuid(), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.NotFound); - } - - [Fact] - public async Task ValidateAndLoad_when_intent_belongs_to_other_user_returns_not_found() - { - await using var ctx = new SqliteTestContext(); - var ownerId = Guid.NewGuid(); - var intent = TestData.UploadIntent(ownerId); - ctx.Database.UploadIntents.Add(intent); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = CreateSut(ctx.Database); - var result = await sut.ValidateAndLoadAsync( - TestData.UploadIntentConfig(), intent.Id, Guid.NewGuid(), CancellationToken.None); - - result.Status.Should().Be(ResultStatus.NotFound); - } - - [Fact] - public async Task ValidateAndLoad_when_consumed_returns_conflict() - { - await using var ctx = new SqliteTestContext(); - var userId = Guid.NewGuid(); - var intent = TestData.UploadIntent(userId, status: UploadIntentStatus.Consumed); - ctx.Database.UploadIntents.Add(intent); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = CreateSut(ctx.Database); - var result = await sut.ValidateAndLoadAsync( - TestData.UploadIntentConfig(), intent.Id, userId, CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Conflict); - } - - [Fact] - public async Task ValidateAndLoad_when_status_expired_returns_invalid() - { - await using var ctx = new SqliteTestContext(); - var userId = Guid.NewGuid(); - var intent = TestData.UploadIntent(userId, status: UploadIntentStatus.Expired); - ctx.Database.UploadIntents.Add(intent); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = CreateSut(ctx.Database); - var result = await sut.ValidateAndLoadAsync( - TestData.UploadIntentConfig(), intent.Id, userId, CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Invalid); - } - - [Fact] - public async Task ValidateAndLoad_when_expired_by_timestamp_returns_invalid() - { - await using var ctx = new SqliteTestContext(); - var userId = Guid.NewGuid(); - var intent = TestData.UploadIntent(userId, status: UploadIntentStatus.Issued, expiresAt: DateTime.UtcNow.AddMinutes(-1)); - ctx.Database.UploadIntents.Add(intent); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - var sut = CreateSut(ctx.Database); - var result = await sut.ValidateAndLoadAsync( - TestData.UploadIntentConfig(), intent.Id, userId, CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Invalid); - } - - [Fact] - public async Task ValidateAndLoad_when_object_missing_in_storage_returns_not_found() - { - await using var ctx = new SqliteTestContext(); - var userId = Guid.NewGuid(); - var intent = TestData.UploadIntent(userId); - ctx.Database.UploadIntents.Add(intent); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - _storage.HeadObjectAsync(intent.Bucket, intent.Key, Arg.Any()) - .Returns((ObjectMetaData?)null); - - var sut = CreateSut(ctx.Database); - var result = await sut.ValidateAndLoadAsync( - TestData.UploadIntentConfig(), intent.Id, userId, CancellationToken.None); - - result.Status.Should().Be(ResultStatus.NotFound); - } - - [Fact] - public async Task ValidateAndLoad_when_object_exceeds_max_size_returns_invalid() - { - await using var ctx = new SqliteTestContext(); - var userId = Guid.NewGuid(); - var config = TestData.UploadIntentConfig(); - config.MaxUploadBytes = 1_000; - var intent = TestData.UploadIntent(userId); - ctx.Database.UploadIntents.Add(intent); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - _storage.HeadObjectAsync(intent.Bucket, intent.Key, Arg.Any()) - .Returns(new ObjectMetaData("image/webp", 2_000)); - - var sut = CreateSut(ctx.Database); - var result = await sut.ValidateAndLoadAsync(config, intent.Id, userId, CancellationToken.None); - - result.Status.Should().Be(ResultStatus.Invalid); - } - - [Fact] - public async Task ValidateAndLoad_happy_path_returns_loaded_intent() - { - await using var ctx = new SqliteTestContext(); - var userId = Guid.NewGuid(); - var intent = TestData.UploadIntent(userId); - ctx.Database.UploadIntents.Add(intent); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - _storage.HeadObjectAsync(intent.Bucket, intent.Key, Arg.Any()) - .Returns(new ObjectMetaData("image/webp", 500)); - - var sut = CreateSut(ctx.Database); - var result = await sut.ValidateAndLoadAsync( - TestData.UploadIntentConfig(), intent.Id, userId, CancellationToken.None); - - result.IsSuccess.Should().BeTrue(); - result.Value.Id.Should().Be(intent.Id); - } -} diff --git a/Tests/Musify.Infrastructure.IntegrationTests/Musify.Infrastructure.IntegrationTests.csproj b/Tests/Musify.Infrastructure.IntegrationTests/Musify.Infrastructure.IntegrationTests.csproj deleted file mode 100644 index beb1cda..0000000 --- a/Tests/Musify.Infrastructure.IntegrationTests/Musify.Infrastructure.IntegrationTests.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - - net10.0 - enable - enable - false - Musify.Infrastructure.IntegrationTests - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - diff --git a/Tests/Musify.Infrastructure.IntegrationTests/SeaweedFsFixture.cs b/Tests/Musify.Infrastructure.IntegrationTests/SeaweedFsFixture.cs deleted file mode 100644 index fcca425..0000000 --- a/Tests/Musify.Infrastructure.IntegrationTests/SeaweedFsFixture.cs +++ /dev/null @@ -1,67 +0,0 @@ -using Amazon.S3; -using Amazon.S3.Model; -using DotNet.Testcontainers.Builders; -using DotNet.Testcontainers.Containers; -using Musify.Infrastructure.Configuration; -using Xunit; - -namespace Musify.Infrastructure.IntegrationTests; - -public sealed class SeaweedFsFixture : IAsyncLifetime -{ - private const int S3Port = 8333; - private const string AccessKey = "admin_access_key"; - private const string SecretKey = "admin_secret_key"; - - public const string Bucket = "webapi-storage"; - - private readonly IContainer _container = new ContainerBuilder() - .WithImage("chrislusf/seaweedfs:latest") - .WithCommand("server", "-s3", "-dir=/data", "-ip.bind=0.0.0.0") - .WithPortBinding(S3Port, true) - .WithWaitStrategy(Wait.ForUnixContainer().UntilMessageIsLogged("Start Seaweed S3 API Server")) - .Build(); - - public string ServiceUrl { get; private set; } = string.Empty; - - public IAmazonS3 CreateClient() - { - var config = new AmazonS3Config - { - ServiceURL = ServiceUrl, - ForcePathStyle = true, - UseHttp = true, - AuthenticationRegion = "us-east-1", - }; - return new AmazonS3Client(AccessKey, SecretKey, config); - } - - public InfrastructureStorageConfiguration StorageConfig() => new() - { - Address = ServiceUrl, - AccessKey = AccessKey, - SecretAccessKey = SecretKey, - ForcePathStyle = true, - UseHttp = true, - }; - - public async Task InitializeAsync() - { - await _container.StartAsync(); - ServiceUrl = $"http://{_container.Hostname}:{_container.GetMappedPublicPort(S3Port)}"; - - using var client = CreateClient(); - await client.PutBucketAsync(new PutBucketRequest { BucketName = Bucket }); - } - - public async Task DisposeAsync() - { - await _container.DisposeAsync(); - } -} - -[CollectionDefinition(SeaweedFsCollection.Name)] -public sealed class SeaweedFsCollection : ICollectionFixture -{ - public const string Name = "SeaweedFS"; -} diff --git a/Tests/Musify.Infrastructure.IntegrationTests/StorageServiceTests.cs b/Tests/Musify.Infrastructure.IntegrationTests/StorageServiceTests.cs deleted file mode 100644 index e3bbbbb..0000000 --- a/Tests/Musify.Infrastructure.IntegrationTests/StorageServiceTests.cs +++ /dev/null @@ -1,145 +0,0 @@ -using System.Text; -using FluentAssertions; -using Microsoft.Extensions.Logging.Abstractions; -using Musify.Application.Contracts; -using Musify.Application.Tests.Infrastructure; -using Musify.Domain.ValueObjects; -using Musify.Infrastructure.Services; -using Xunit; - -namespace Musify.Infrastructure.IntegrationTests; - -[Collection(SeaweedFsCollection.Name)] -public sealed class StorageServiceTests(SeaweedFsFixture seaweedFs) -{ - private const string Bucket = SeaweedFsFixture.Bucket; - - private (StorageService service, SqliteTestContext db) CreateService() - { - var db = new SqliteTestContext(); - var service = new StorageService( - db.Database, - seaweedFs.CreateClient(), - NullLogger.Instance, - seaweedFs.StorageConfig()); - return (service, db); - } - - private static MemoryStream TextStream(string content) => new(Encoding.UTF8.GetBytes(content)); - - [Fact] - public async Task Upload_then_head_and_get_roundtrips_content() - { - var (service, db) = CreateService(); - using var _ = db; - var key = $"tests/{Guid.NewGuid()}.txt"; - - await service.UploadFileAsync(TextStream("hello seaweed"), "text/plain", Bucket, key, CancellationToken.None); - - var metadata = await service.HeadObjectAsync(Bucket, key, CancellationToken.None); - metadata.Should().NotBeNull(); - metadata!.ContentLength.Should().Be("hello seaweed".Length); - - using var stream = await service.GetFileAsync(Bucket, key, CancellationToken.None); - using var reader = new StreamReader(stream); - (await reader.ReadToEndAsync()).Should().Be("hello seaweed"); - } - - [Fact] - public async Task Upload_persists_a_successful_upload_record() - { - var (service, db) = CreateService(); - using var _ = db; - var key = $"tests/{Guid.NewGuid()}.txt"; - - await service.UploadFileAsync(TextStream("data"), "text/plain", Bucket, key, CancellationToken.None); - - var upload = db.NewContext().Uploads.Single(u => u.Key == key); - upload.State.Should().Be(UploadState.Successful); - upload.Bucket.Should().Be(Bucket); - } - - [Fact] - public async Task HeadObject_returns_null_for_missing_object() - { - var (service, db) = CreateService(); - using var _ = db; - - var metadata = await service.HeadObjectAsync(Bucket, $"tests/missing-{Guid.NewGuid()}", CancellationToken.None); - - metadata.Should().BeNull(); - } - - [Fact] - public async Task CopyFile_duplicates_the_object() - { - var (service, db) = CreateService(); - using var _ = db; - var source = $"tests/{Guid.NewGuid()}-src.txt"; - var destination = $"tests/{Guid.NewGuid()}-dst.txt"; - await service.UploadFileAsync(TextStream("copy me"), "text/plain", Bucket, source, CancellationToken.None); - - await service.CopyFileAsync(Bucket, source, Bucket, destination, CancellationToken.None); - - (await service.HeadObjectAsync(Bucket, destination, CancellationToken.None)).Should().NotBeNull(); - (await service.HeadObjectAsync(Bucket, source, CancellationToken.None)).Should().NotBeNull(); - } - - [Fact] - public async Task RemoveFile_deletes_the_object() - { - var (service, db) = CreateService(); - using var _ = db; - var key = $"tests/{Guid.NewGuid()}.txt"; - await service.UploadFileAsync(TextStream("delete me"), "text/plain", Bucket, key, CancellationToken.None); - - await service.RemoveFileAsync(Bucket, key, CancellationToken.None); - - (await service.HeadObjectAsync(Bucket, key, CancellationToken.None)).Should().BeNull(); - } - - [Fact] - public async Task ListObjects_returns_objects_under_prefix() - { - var (service, db) = CreateService(); - using var _ = db; - var prefix = $"listing/{Guid.NewGuid()}"; - await service.UploadFileAsync(TextStream("a"), "text/plain", Bucket, $"{prefix}/a.txt", CancellationToken.None); - await service.UploadFileAsync(TextStream("b"), "text/plain", Bucket, $"{prefix}/b.txt", CancellationToken.None); - - var keys = new List(); - await foreach (var obj in service.ListObjectsAsync(Bucket, prefix, CancellationToken.None)) - keys.Add(obj.Key); - - keys.Should().Contain($"{prefix}/a.txt").And.Contain($"{prefix}/b.txt"); - } - - [Fact] - public async Task RemoveFolder_deletes_every_object_under_prefix() - { - var (service, db) = CreateService(); - using var _ = db; - var prefix = $"folder/{Guid.NewGuid()}"; - await service.UploadFileAsync(TextStream("a"), "text/plain", Bucket, $"{prefix}/a.txt", CancellationToken.None); - await service.UploadFileAsync(TextStream("b"), "text/plain", Bucket, $"{prefix}/nested/b.txt", CancellationToken.None); - - await service.RemoveFolderAsync(Bucket, prefix, CancellationToken.None); - - var remaining = new List(); - await foreach (var obj in service.ListObjectsAsync(Bucket, prefix, CancellationToken.None)) - remaining.Add(obj); - remaining.Should().BeEmpty(); - } - - [Fact] - public async Task GetUploadUrl_returns_a_presigned_url() - { - var (service, db) = CreateService(); - using var _ = db; - - var url = await service.GetUploadUrlAsync( - Bucket, $"tests/{Guid.NewGuid()}.txt", "text/plain", TimeSpan.FromMinutes(5), CancellationToken.None); - - url.Should().StartWith(seaweedFs.ServiceUrl); - } -} diff --git a/Tests/Musify.Infrastructure.Tests/JobTestHarness.cs b/Tests/Musify.Infrastructure.Tests/JobTestHarness.cs deleted file mode 100644 index 2e5fe4a..0000000 --- a/Tests/Musify.Infrastructure.Tests/JobTestHarness.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Reflection; -using Microsoft.Extensions.DependencyInjection; -using Musify.Application.Contracts; -using Musify.Application.Tests.Infrastructure; - -namespace Musify.Infrastructure.Tests; - -internal static class JobTestHarness -{ - public static IServiceScopeFactory ScopeFactoryWith(Action configure) - { - var services = new ServiceCollection(); - configure(services); - return services.BuildServiceProvider().GetRequiredService(); - } - - public static IServiceScopeFactory WithDatabase(IDatabase database) => - ScopeFactoryWith(s => s.AddSingleton(database)); - - public static IServiceScopeFactory WithStorage(IStorageService storage) => - ScopeFactoryWith(s => s.AddSingleton(storage)); - - public static Task RunAsync(object job, CancellationToken cancellationToken = default) - { - var method = job.GetType().GetMethod( - "RunAsync", - BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public) - ?? throw new InvalidOperationException("RunAsync not found on job."); - - return (Task)method.Invoke(job, [cancellationToken])!; - } -} diff --git a/Tests/Musify.Infrastructure.Tests/Musify.Infrastructure.Tests.csproj b/Tests/Musify.Infrastructure.Tests/Musify.Infrastructure.Tests.csproj deleted file mode 100644 index 820228f..0000000 --- a/Tests/Musify.Infrastructure.Tests/Musify.Infrastructure.Tests.csproj +++ /dev/null @@ -1,29 +0,0 @@ - - - - net10.0 - enable - enable - false - Musify.Infrastructure.Tests - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - diff --git a/Tests/Musify.Infrastructure.Tests/TemporalUploadsCleanUpJobTests.cs b/Tests/Musify.Infrastructure.Tests/TemporalUploadsCleanUpJobTests.cs deleted file mode 100644 index 2b255d3..0000000 --- a/Tests/Musify.Infrastructure.Tests/TemporalUploadsCleanUpJobTests.cs +++ /dev/null @@ -1,72 +0,0 @@ -using FluentAssertions; -using Microsoft.Extensions.Logging.Abstractions; -using Musify.Application.Contracts; -using Musify.Application.Tests.Infrastructure; -using Musify.Infrastructure.Jobs; -using NSubstitute; -using Xunit; - -namespace Musify.Infrastructure.Tests; - -public sealed class TemporalUploadsCleanUpJobTests -{ - private readonly IStorageService _storage = Substitute.For(); - private readonly Application.Configuration.UploadIntentConfiguration _config = TestData.UploadIntentConfig(); - - private TemporalUploadsCleanUpJob CreateJob() => - new( - JobTestHarness.WithStorage(_storage), - NullLogger.Instance, - TestData.StorageConfig(), - _config); - - private static async IAsyncEnumerable AsAsync(params StorageObject[] items) - { - foreach (var item in items) - yield return item; - await Task.CompletedTask; - } - - [Fact] - public async Task Deletes_only_objects_older_than_retention_window() - { - var cutoffDays = _config.TempUploadsRetentionDays; - var stale = new StorageObject("temp/u1/old.webp", DateTime.UtcNow.AddDays(-(cutoffDays + 2))); - var fresh = new StorageObject("temp/u1/new.webp", DateTime.UtcNow.AddHours(-1)); - - _storage.ListObjectsAsync(TestData.Bucket, _config.TempRootPrefix, Arg.Any()) - .Returns(AsAsync(stale, fresh)); - - await JobTestHarness.RunAsync(CreateJob()); - - await _storage.Received(1).RemoveFileAsync(TestData.Bucket, stale.Key, Arg.Any()); - await _storage.DidNotReceive().RemoveFileAsync(TestData.Bucket, fresh.Key, Arg.Any()); - } - - [Fact] - public async Task Continues_when_a_delete_fails() - { - var first = new StorageObject("temp/u1/a.webp", DateTime.UtcNow.AddDays(-10)); - var second = new StorageObject("temp/u1/b.webp", DateTime.UtcNow.AddDays(-10)); - - _storage.ListObjectsAsync(TestData.Bucket, _config.TempRootPrefix, Arg.Any()) - .Returns(AsAsync(first, second)); - _storage.RemoveFileAsync(TestData.Bucket, first.Key, Arg.Any()) - .Returns(_ => throw new InvalidOperationException("s3 down")); - - await JobTestHarness.RunAsync(CreateJob()); - - await _storage.Received(1).RemoveFileAsync(TestData.Bucket, second.Key, Arg.Any()); - } - - [Fact] - public async Task Does_nothing_when_no_objects_listed() - { - _storage.ListObjectsAsync(TestData.Bucket, _config.TempRootPrefix, Arg.Any()) - .Returns(AsAsync()); - - await JobTestHarness.RunAsync(CreateJob()); - - await _storage.DidNotReceive().RemoveFileAsync(Arg.Any(), Arg.Any(), Arg.Any()); - } -} diff --git a/Tests/Musify.Infrastructure.Tests/UploadIntentExpirationJobTests.cs b/Tests/Musify.Infrastructure.Tests/UploadIntentExpirationJobTests.cs deleted file mode 100644 index cca62fb..0000000 --- a/Tests/Musify.Infrastructure.Tests/UploadIntentExpirationJobTests.cs +++ /dev/null @@ -1,84 +0,0 @@ -using FluentAssertions; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging.Abstractions; -using Musify.Application.Tests.Infrastructure; -using Musify.Domain.ValueObjects; -using Musify.Infrastructure.Jobs; -using Xunit; - -namespace Musify.Infrastructure.Tests; - -public sealed class UploadIntentExpirationJobTests -{ - private static UploadIntentExpirationJob CreateJob(SqliteTestContext ctx, Action? configure = null) - { - var config = TestData.UploadIntentConfig(); - configure?.Invoke(config); - return new UploadIntentExpirationJob( - JobTestHarness.WithDatabase(ctx.Database), - NullLogger.Instance, - config); - } - - [Fact] - public async Task Marks_issued_intents_past_expiry_as_expired() - { - await using var ctx = new SqliteTestContext(); - var expired = TestData.UploadIntent(Guid.NewGuid(), status: UploadIntentStatus.Issued, expiresAt: DateTime.UtcNow.AddMinutes(-5)); - var active = TestData.UploadIntent(Guid.NewGuid(), status: UploadIntentStatus.Issued, expiresAt: DateTime.UtcNow.AddMinutes(30)); - ctx.Database.UploadIntents.AddRange(expired, active); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - await JobTestHarness.RunAsync(CreateJob(ctx)); - - var verify = ctx.NewContext(); - (await verify.UploadIntents.SingleAsync(i => i.Id == expired.Id)).Status.Should().Be(UploadIntentStatus.Expired); - (await verify.UploadIntents.SingleAsync(i => i.Id == active.Id)).Status.Should().Be(UploadIntentStatus.Issued); - } - - [Fact] - public async Task Deletes_expired_intents_past_retention_window() - { - await using var ctx = new SqliteTestContext(); - var config = TestData.UploadIntentConfig(); - var old = TestData.UploadIntent(Guid.NewGuid(), status: UploadIntentStatus.Expired, - expiresAt: DateTime.UtcNow.AddDays(-(config.ExpiredIntentsRetentionDays + 1))); - var recent = TestData.UploadIntent(Guid.NewGuid(), status: UploadIntentStatus.Expired, - expiresAt: DateTime.UtcNow.AddHours(-1)); - ctx.Database.UploadIntents.AddRange(old, recent); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - await JobTestHarness.RunAsync(CreateJob(ctx)); - - var verify = ctx.NewContext(); - (await verify.UploadIntents.AnyAsync(i => i.Id == old.Id)).Should().BeFalse(); - (await verify.UploadIntents.AnyAsync(i => i.Id == recent.Id)).Should().BeTrue(); - } - - [Fact] - public async Task With_zero_retention_just_expired_intents_are_marked_then_deleted() - { - await using var ctx = new SqliteTestContext(); - var intent = TestData.UploadIntent(Guid.NewGuid(), status: UploadIntentStatus.Issued, expiresAt: DateTime.UtcNow.AddMinutes(-1)); - ctx.Database.UploadIntents.Add(intent); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - await JobTestHarness.RunAsync(CreateJob(ctx, c => c.ExpiredIntentsRetentionDays = 0)); - - (await ctx.NewContext().UploadIntents.AnyAsync(i => i.Id == intent.Id)).Should().BeFalse(); - } - - [Fact] - public async Task Leaves_consumed_intents_untouched() - { - await using var ctx = new SqliteTestContext(); - var consumed = TestData.UploadIntent(Guid.NewGuid(), status: UploadIntentStatus.Consumed, expiresAt: DateTime.UtcNow.AddDays(-10)); - ctx.Database.UploadIntents.Add(consumed); - await ctx.Database.SaveChangesAsync(CancellationToken.None); - - await JobTestHarness.RunAsync(CreateJob(ctx)); - - (await ctx.NewContext().UploadIntents.SingleAsync(i => i.Id == consumed.Id)).Status - .Should().Be(UploadIntentStatus.Consumed); - } -}