Add editorconfig and analyzers, format backend code
This commit is contained in:
@@ -3,83 +3,82 @@ using Microsoft.AspNetCore.Http;
|
||||
using Musify.Api.Authentication;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Api.Tests.Authentication
|
||||
namespace Musify.Api.Tests.Authentication;
|
||||
|
||||
public sealed class CurrentUserTests
|
||||
{
|
||||
public sealed class CurrentUserTests
|
||||
private static async Task<CurrentUser> BindAsync(ClaimsPrincipal principal)
|
||||
{
|
||||
private static async Task<CurrentUser> BindAsync(ClaimsPrincipal principal)
|
||||
{
|
||||
var context = new DefaultHttpContext { User = principal };
|
||||
var bound = await CurrentUser.BindAsync(context, parameter: null!);
|
||||
return bound!;
|
||||
}
|
||||
var context = new DefaultHttpContext { User = principal };
|
||||
var bound = await CurrentUser.BindAsync(context, parameter: null!);
|
||||
return bound!;
|
||||
}
|
||||
|
||||
private static ClaimsPrincipal AuthenticatedPrincipal(params Claim[] claims) =>
|
||||
new(new ClaimsIdentity(claims, authenticationType: "TestAuth"));
|
||||
private static ClaimsPrincipal AuthenticatedPrincipal(params Claim[] claims) =>
|
||||
new(new ClaimsIdentity(claims, authenticationType: "TestAuth"));
|
||||
|
||||
[Fact]
|
||||
public async Task BindAsync_AuthenticatedPrincipalWithValidClaims_PopulatesEveryField()
|
||||
{
|
||||
var principal = AuthenticatedPrincipal(
|
||||
new Claim(ClaimTypes.NameIdentifier, "42"),
|
||||
new Claim(ClaimTypes.Name, "jdoe"),
|
||||
new Claim(ClaimTypes.GivenName, "Jane"),
|
||||
new Claim(ClaimTypes.Surname, "Doe"));
|
||||
[Fact]
|
||||
public async Task BindAsync_AuthenticatedPrincipalWithValidClaims_PopulatesEveryField()
|
||||
{
|
||||
var principal = AuthenticatedPrincipal(
|
||||
new Claim(ClaimTypes.NameIdentifier, "42"),
|
||||
new Claim(ClaimTypes.Name, "jdoe"),
|
||||
new Claim(ClaimTypes.GivenName, "Jane"),
|
||||
new Claim(ClaimTypes.Surname, "Doe"));
|
||||
|
||||
var user = await BindAsync(principal);
|
||||
var user = await BindAsync(principal);
|
||||
|
||||
Assert.True(user.IsAuthenticated);
|
||||
Assert.Equal(42, user.Id);
|
||||
Assert.Equal(42, user.RequiredId);
|
||||
Assert.Equal("jdoe", user.Username);
|
||||
Assert.Equal("Jane", user.FirstName);
|
||||
Assert.Equal("Doe", user.LastName);
|
||||
}
|
||||
Assert.True(user.IsAuthenticated);
|
||||
Assert.Equal(42, user.Id);
|
||||
Assert.Equal(42, user.RequiredId);
|
||||
Assert.Equal("jdoe", user.Username);
|
||||
Assert.Equal("Jane", user.FirstName);
|
||||
Assert.Equal("Doe", user.LastName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BindAsync_AnonymousPrincipal_IsNotAuthenticatedAndHasNoId()
|
||||
{
|
||||
var user = await BindAsync(new ClaimsPrincipal(new ClaimsIdentity()));
|
||||
[Fact]
|
||||
public async Task BindAsync_AnonymousPrincipal_IsNotAuthenticatedAndHasNoId()
|
||||
{
|
||||
var user = await BindAsync(new ClaimsPrincipal(new ClaimsIdentity()));
|
||||
|
||||
Assert.False(user.IsAuthenticated);
|
||||
Assert.Null(user.Id);
|
||||
}
|
||||
Assert.False(user.IsAuthenticated);
|
||||
Assert.Null(user.Id);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("not-a-number")]
|
||||
[InlineData("0")]
|
||||
[InlineData("")]
|
||||
public async Task BindAsync_NonNumericOrZeroNameIdentifier_LeavesIdNull(string rawId)
|
||||
{
|
||||
var user = await BindAsync(AuthenticatedPrincipal(new Claim(ClaimTypes.NameIdentifier, rawId)));
|
||||
[Theory]
|
||||
[InlineData("not-a-number")]
|
||||
[InlineData("0")]
|
||||
[InlineData("")]
|
||||
public async Task BindAsync_NonNumericOrZeroNameIdentifier_LeavesIdNull(string rawId)
|
||||
{
|
||||
var user = await BindAsync(AuthenticatedPrincipal(new Claim(ClaimTypes.NameIdentifier, rawId)));
|
||||
|
||||
Assert.Null(user.Id);
|
||||
}
|
||||
Assert.Null(user.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RequiredId_MissingId_Throws()
|
||||
{
|
||||
var user = await BindAsync(new ClaimsPrincipal(new ClaimsIdentity()));
|
||||
[Fact]
|
||||
public async Task RequiredId_MissingId_Throws()
|
||||
{
|
||||
var user = await BindAsync(new ClaimsPrincipal(new ClaimsIdentity()));
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => user.RequiredId);
|
||||
}
|
||||
Assert.Throws<InvalidOperationException>(() => user.RequiredId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HasClaim_ExistingClaimType_ReturnsTrue()
|
||||
{
|
||||
var user = await BindAsync(AuthenticatedPrincipal(new Claim("role", "admin")));
|
||||
[Fact]
|
||||
public async Task HasClaim_ExistingClaimType_ReturnsTrue()
|
||||
{
|
||||
var user = await BindAsync(AuthenticatedPrincipal(new Claim("role", "admin")));
|
||||
|
||||
Assert.True(user.HasClaim("role"));
|
||||
Assert.False(user.HasClaim("missing"));
|
||||
}
|
||||
Assert.True(user.HasClaim("role"));
|
||||
Assert.False(user.HasClaim("missing"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindClaim_ExistingClaimType_ReturnsItsValue()
|
||||
{
|
||||
var user = await BindAsync(AuthenticatedPrincipal(new Claim("picture", "https://avatar.example/jane.png")));
|
||||
[Fact]
|
||||
public async Task FindClaim_ExistingClaimType_ReturnsItsValue()
|
||||
{
|
||||
var user = await BindAsync(AuthenticatedPrincipal(new Claim("picture", "https://avatar.example/jane.png")));
|
||||
|
||||
Assert.Equal("https://avatar.example/jane.png", user.FindClaim("picture"));
|
||||
Assert.Null(user.FindClaim("missing"));
|
||||
}
|
||||
Assert.Equal("https://avatar.example/jane.png", user.FindClaim("picture"));
|
||||
Assert.Null(user.FindClaim("missing"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,129 +8,128 @@ using Musify.Application.Contracts;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Api.Tests.Endpoints
|
||||
namespace Musify.Api.Tests.Endpoints;
|
||||
|
||||
[Collection(ApiCollection.Name)]
|
||||
public sealed class AlbumEndpointsTests(ApiTestFixture fixture)
|
||||
{
|
||||
[Collection(ApiCollection.Name)]
|
||||
public sealed class AlbumEndpointsTests(ApiTestFixture fixture)
|
||||
private async Task<long> CreateUserAsync()
|
||||
{
|
||||
private async Task<long> CreateUserAsync()
|
||||
{
|
||||
var userId = Random.Shared.NextInt64(1, long.MaxValue);
|
||||
var response = await fixture.CreateAnonymousClient()
|
||||
.PostAsJsonAsync("/users", new CreateUserRequest(userId, $"user-{userId}", null, null));
|
||||
response.EnsureSuccessStatusCode();
|
||||
return userId;
|
||||
}
|
||||
var userId = Random.Shared.NextInt64(1, long.MaxValue);
|
||||
var response = await fixture.CreateAnonymousClient()
|
||||
.PostAsJsonAsync("/users", new CreateUserRequest(userId, $"user-{userId}", null, null));
|
||||
response.EnsureSuccessStatusCode();
|
||||
return userId;
|
||||
}
|
||||
|
||||
private async Task<Guid> CreatePictureIntentAsync(HttpClient client)
|
||||
{
|
||||
fixture.StorageService
|
||||
.GetUploadUrlAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<TimeSpan>(), Arg.Any<CancellationToken>())
|
||||
.Returns("https://storage.musify.test/presigned-upload");
|
||||
fixture.StorageService
|
||||
.HeadObjectAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new ObjectMetaData("image/webp", 1024));
|
||||
private async Task<Guid> CreatePictureIntentAsync(HttpClient client)
|
||||
{
|
||||
fixture.StorageService
|
||||
.GetUploadUrlAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<TimeSpan>(), Arg.Any<CancellationToken>())
|
||||
.Returns("https://storage.musify.test/presigned-upload");
|
||||
fixture.StorageService
|
||||
.HeadObjectAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new ObjectMetaData("image/webp", 1024));
|
||||
|
||||
var response = await client.PostAsJsonAsync(
|
||||
"/albums/upload-picture",
|
||||
new RequestAlbumPictureUploadRequest("webp", "image/webp"),
|
||||
TestContext.Current.CancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var response = await client.PostAsJsonAsync(
|
||||
"/albums/upload-picture",
|
||||
new RequestAlbumPictureUploadRequest("webp", "image/webp"),
|
||||
TestContext.Current.CancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var body = await response.Content.ReadFromJsonAsync<AlbumPictureUploadResponse>(TestContext.Current.CancellationToken);
|
||||
return body!.IntentId;
|
||||
}
|
||||
var body = await response.Content.ReadFromJsonAsync<AlbumPictureUploadResponse>(TestContext.Current.CancellationToken);
|
||||
return body!.IntentId;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PostAlbums_NoAuthHeader_ReturnsUnauthorized()
|
||||
{
|
||||
var response = await fixture.CreateAnonymousClient()
|
||||
.PostAsJsonAsync("/albums", new CreateAlbumRequest("Unauthorized Album", null, null, Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task PostAlbums_NoAuthHeader_ReturnsUnauthorized()
|
||||
{
|
||||
var response = await fixture.CreateAnonymousClient()
|
||||
.PostAsJsonAsync("/albums", new CreateAlbumRequest("Unauthorized Album", null, null, Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||
}
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PostAlbums_AuthenticatedExistingUser_ReturnsCreatedAlbum()
|
||||
{
|
||||
var userId = await CreateUserAsync();
|
||||
var client = fixture.CreateAuthenticatedClient(userId);
|
||||
var pictureIntentId = await CreatePictureIntentAsync(client);
|
||||
[Fact]
|
||||
public async Task PostAlbums_AuthenticatedExistingUser_ReturnsCreatedAlbum()
|
||||
{
|
||||
var userId = await CreateUserAsync();
|
||||
var client = fixture.CreateAuthenticatedClient(userId);
|
||||
var pictureIntentId = await CreatePictureIntentAsync(client);
|
||||
|
||||
var response = await client.PostAsJsonAsync("/albums", new CreateAlbumRequest("My Album", "desc", 2024, pictureIntentId), TestContext.Current.CancellationToken);
|
||||
var response = await client.PostAsJsonAsync("/albums", new CreateAlbumRequest("My Album", "desc", 2024, pictureIntentId), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
|
||||
var body = await response.Content.ReadFromJsonAsync<AlbumApplicationResponse>(TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(body);
|
||||
Assert.Equal("My Album", body.Title);
|
||||
Assert.Equal(userId, body.OwnerUserId);
|
||||
}
|
||||
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
|
||||
var body = await response.Content.ReadFromJsonAsync<AlbumApplicationResponse>(TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(body);
|
||||
Assert.Equal("My Album", body.Title);
|
||||
Assert.Equal(userId, body.OwnerUserId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PostAlbums_BlankTitle_ReturnsValidationProblem()
|
||||
{
|
||||
var userId = await CreateUserAsync();
|
||||
var client = fixture.CreateAuthenticatedClient(userId);
|
||||
[Fact]
|
||||
public async Task PostAlbums_BlankTitle_ReturnsValidationProblem()
|
||||
{
|
||||
var userId = await CreateUserAsync();
|
||||
var client = fixture.CreateAuthenticatedClient(userId);
|
||||
|
||||
var response = await client.PostAsJsonAsync("/albums", new CreateAlbumRequest("", null, null, Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||
var response = await client.PostAsJsonAsync("/albums", new CreateAlbumRequest("", null, null, Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PostAlbums_MissingPictureIntent_ReturnsValidationProblem()
|
||||
{
|
||||
var userId = await CreateUserAsync();
|
||||
var client = fixture.CreateAuthenticatedClient(userId);
|
||||
[Fact]
|
||||
public async Task PostAlbums_MissingPictureIntent_ReturnsValidationProblem()
|
||||
{
|
||||
var userId = await CreateUserAsync();
|
||||
var client = fixture.CreateAuthenticatedClient(userId);
|
||||
|
||||
var response = await client.PostAsJsonAsync("/albums", new CreateAlbumRequest("No Picture", null, null, Guid.Empty), TestContext.Current.CancellationToken);
|
||||
var response = await client.PostAsJsonAsync("/albums", new CreateAlbumRequest("No Picture", null, null, Guid.Empty), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAlbumById_AfterCreate_ReturnsIt()
|
||||
{
|
||||
var userId = await CreateUserAsync();
|
||||
var client = fixture.CreateAuthenticatedClient(userId);
|
||||
var pictureIntentId = await CreatePictureIntentAsync(client);
|
||||
var created = await (await client.PostAsJsonAsync("/albums", new CreateAlbumRequest("Findable", null, null, pictureIntentId), TestContext.Current.CancellationToken))
|
||||
.Content.ReadFromJsonAsync<AlbumApplicationResponse>(TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task GetAlbumById_AfterCreate_ReturnsIt()
|
||||
{
|
||||
var userId = await CreateUserAsync();
|
||||
var client = fixture.CreateAuthenticatedClient(userId);
|
||||
var pictureIntentId = await CreatePictureIntentAsync(client);
|
||||
var created = await (await client.PostAsJsonAsync("/albums", new CreateAlbumRequest("Findable", null, null, pictureIntentId), TestContext.Current.CancellationToken))
|
||||
.Content.ReadFromJsonAsync<AlbumApplicationResponse>(TestContext.Current.CancellationToken);
|
||||
|
||||
var response = await fixture.CreateAnonymousClient().GetAsync($"/albums/{created!.Id}", TestContext.Current.CancellationToken);
|
||||
var response = await fixture.CreateAnonymousClient().GetAsync($"/albums/{created!.Id}", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
}
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteAlbum_NotTheOwner_ReturnsUnauthorized()
|
||||
{
|
||||
var owner = await CreateUserAsync();
|
||||
var stranger = await CreateUserAsync();
|
||||
var ownerClient = fixture.CreateAuthenticatedClient(owner);
|
||||
var pictureIntentId = await CreatePictureIntentAsync(ownerClient);
|
||||
var created = await (await ownerClient.PostAsJsonAsync("/albums", new CreateAlbumRequest("Owned", null, null, pictureIntentId), TestContext.Current.CancellationToken))
|
||||
.Content.ReadFromJsonAsync<AlbumApplicationResponse>(TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task DeleteAlbum_NotTheOwner_ReturnsUnauthorized()
|
||||
{
|
||||
var owner = await CreateUserAsync();
|
||||
var stranger = await CreateUserAsync();
|
||||
var ownerClient = fixture.CreateAuthenticatedClient(owner);
|
||||
var pictureIntentId = await CreatePictureIntentAsync(ownerClient);
|
||||
var created = await (await ownerClient.PostAsJsonAsync("/albums", new CreateAlbumRequest("Owned", null, null, pictureIntentId), TestContext.Current.CancellationToken))
|
||||
.Content.ReadFromJsonAsync<AlbumApplicationResponse>(TestContext.Current.CancellationToken);
|
||||
|
||||
var response = await fixture.CreateAuthenticatedClient(stranger).DeleteAsync($"/albums/{created!.Id}", TestContext.Current.CancellationToken);
|
||||
var response = await fixture.CreateAuthenticatedClient(stranger).DeleteAsync($"/albums/{created!.Id}", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||
}
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteAlbum_Owner_ReturnsNoContentAndAlbumIsGone()
|
||||
{
|
||||
var userId = await CreateUserAsync();
|
||||
var client = fixture.CreateAuthenticatedClient(userId);
|
||||
var pictureIntentId = await CreatePictureIntentAsync(client);
|
||||
var created = await (await client.PostAsJsonAsync("/albums", new CreateAlbumRequest("Deletable", null, null, pictureIntentId), TestContext.Current.CancellationToken))
|
||||
.Content.ReadFromJsonAsync<AlbumApplicationResponse>(TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task DeleteAlbum_Owner_ReturnsNoContentAndAlbumIsGone()
|
||||
{
|
||||
var userId = await CreateUserAsync();
|
||||
var client = fixture.CreateAuthenticatedClient(userId);
|
||||
var pictureIntentId = await CreatePictureIntentAsync(client);
|
||||
var created = await (await client.PostAsJsonAsync("/albums", new CreateAlbumRequest("Deletable", null, null, pictureIntentId), TestContext.Current.CancellationToken))
|
||||
.Content.ReadFromJsonAsync<AlbumApplicationResponse>(TestContext.Current.CancellationToken);
|
||||
|
||||
var deleteResponse = await client.DeleteAsync($"/albums/{created!.Id}", TestContext.Current.CancellationToken);
|
||||
var getResponse = await fixture.CreateAnonymousClient().GetAsync($"/albums/{created.Id}", TestContext.Current.CancellationToken);
|
||||
var deleteResponse = await client.DeleteAsync($"/albums/{created!.Id}", TestContext.Current.CancellationToken);
|
||||
var getResponse = await fixture.CreateAnonymousClient().GetAsync($"/albums/{created.Id}", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.NoContent, deleteResponse.StatusCode);
|
||||
Assert.Equal(HttpStatusCode.NotFound, getResponse.StatusCode);
|
||||
}
|
||||
Assert.Equal(HttpStatusCode.NoContent, deleteResponse.StatusCode);
|
||||
Assert.Equal(HttpStatusCode.NotFound, getResponse.StatusCode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,22 +4,21 @@ using Musify.Api.Tests.TestSupport;
|
||||
using Musify.Application.Configuration.Responses;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Api.Tests.Endpoints
|
||||
namespace Musify.Api.Tests.Endpoints;
|
||||
|
||||
[Collection(ApiCollection.Name)]
|
||||
public sealed class ConfigEndpointsTests(ApiTestFixture fixture)
|
||||
{
|
||||
[Collection(ApiCollection.Name)]
|
||||
public sealed class ConfigEndpointsTests(ApiTestFixture fixture)
|
||||
[Fact]
|
||||
public async Task GetPlaybackConfig_WithoutAuthentication_ReturnsConfiguredValues()
|
||||
{
|
||||
[Fact]
|
||||
public async Task GetPlaybackConfig_WithoutAuthentication_ReturnsConfiguredValues()
|
||||
{
|
||||
var client = fixture.CreateAnonymousClient();
|
||||
var client = fixture.CreateAnonymousClient();
|
||||
|
||||
var response = await client.GetAsync("/config/playback", TestContext.Current.CancellationToken);
|
||||
var response = await client.GetAsync("/config/playback", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var body = await response.Content.ReadFromJsonAsync<PlaybackPublicConfigResponse>(TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(body);
|
||||
Assert.False(body.AllowAnonymousListening);
|
||||
}
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var body = await response.Content.ReadFromJsonAsync<PlaybackPublicConfigResponse>(TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(body);
|
||||
Assert.False(body.AllowAnonymousListening);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,96 +11,95 @@ using Musify.Domain.ValueObjects;
|
||||
using Musify.Infrastructure.Persistence;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Api.Tests.Endpoints
|
||||
namespace Musify.Api.Tests.Endpoints;
|
||||
|
||||
[Collection(ApiCollection.Name)]
|
||||
public sealed class GenreEndpointsTests(ApiTestFixture fixture)
|
||||
{
|
||||
[Collection(ApiCollection.Name)]
|
||||
public sealed class GenreEndpointsTests(ApiTestFixture fixture)
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
};
|
||||
|
||||
private async Task<string> SeedTrackAsync(Genre genre)
|
||||
{
|
||||
using var scope = fixture.Services.CreateScope();
|
||||
var database = scope.ServiceProvider.GetRequiredService<Database>();
|
||||
var owner = new User { Id = Random.Shared.NextInt64(1, long.MaxValue), Name = "genre-owner", NormalizedName = "GENRE-OWNER" };
|
||||
var title = $"Genre track {Guid.NewGuid():N}";
|
||||
var track = new Track
|
||||
{
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
Title = title,
|
||||
NormalizedTitle = title.ToUpperInvariant(),
|
||||
OwnerUserId = owner.Id,
|
||||
Owner = owner,
|
||||
Tags = [new TrackTag { TrackId = Guid.Empty, Tag = genre }]
|
||||
};
|
||||
foreach (var tag in track.Tags)
|
||||
tag.TrackId = track.Id;
|
||||
database.Users.Add(owner);
|
||||
database.Tracks.Add(track);
|
||||
database.UserHasTracks.Add(new UserHasTrack { UserId = owner.Id, TrackId = track.Id });
|
||||
await database.SaveChangesAsync(TestContext.Current.CancellationToken);
|
||||
return title;
|
||||
}
|
||||
|
||||
private async Task<string> SeedTrackAsync(Genre genre)
|
||||
{
|
||||
using var scope = fixture.Services.CreateScope();
|
||||
var database = scope.ServiceProvider.GetRequiredService<Database>();
|
||||
var owner = new User { Id = Random.Shared.NextInt64(1, long.MaxValue), Name = "genre-owner", NormalizedName = "GENRE-OWNER" };
|
||||
var title = $"Genre track {Guid.NewGuid():N}";
|
||||
var track = new Track
|
||||
{
|
||||
Title = title,
|
||||
NormalizedTitle = title.ToUpperInvariant(),
|
||||
OwnerUserId = owner.Id,
|
||||
Owner = owner,
|
||||
Tags = [new TrackTag { TrackId = Guid.Empty, Tag = genre }]
|
||||
};
|
||||
foreach (var tag in track.Tags)
|
||||
tag.TrackId = track.Id;
|
||||
database.Users.Add(owner);
|
||||
database.Tracks.Add(track);
|
||||
database.UserHasTracks.Add(new UserHasTrack { UserId = owner.Id, TrackId = track.Id });
|
||||
await database.SaveChangesAsync(TestContext.Current.CancellationToken);
|
||||
return title;
|
||||
}
|
||||
[Fact]
|
||||
public async Task GetGenres_Anonymous_ListsGenresInUse()
|
||||
{
|
||||
await SeedTrackAsync(Genre.Funk);
|
||||
|
||||
[Fact]
|
||||
public async Task GetGenres_Anonymous_ListsGenresInUse()
|
||||
{
|
||||
await SeedTrackAsync(Genre.Funk);
|
||||
var response = await fixture.CreateAnonymousClient().GetAsync("/genres", TestContext.Current.CancellationToken);
|
||||
|
||||
var response = await fixture.CreateAnonymousClient().GetAsync("/genres", TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var body = await response.Content.ReadFromJsonAsync<List<GenreResponse>>(JsonOptions, TestContext.Current.CancellationToken);
|
||||
Assert.Contains(body!, g => g.Genre == Genre.Funk && g.TrackCount >= 1);
|
||||
}
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var body = await response.Content.ReadFromJsonAsync<List<GenreResponse>>(JsonOptions, TestContext.Current.CancellationToken);
|
||||
Assert.Contains(body!, g => g.Genre == Genre.Funk && g.TrackCount >= 1);
|
||||
}
|
||||
[Fact]
|
||||
public async Task GetAvailableGenres_Anonymous_ListsEveryGenreWithIncompatibilities()
|
||||
{
|
||||
var response = await fixture.CreateAnonymousClient().GetAsync("/genres/available", TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task GetAvailableGenres_Anonymous_ListsEveryGenreWithIncompatibilities()
|
||||
{
|
||||
var response = await fixture.CreateAnonymousClient().GetAsync("/genres/available", TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var body = await response.Content.ReadFromJsonAsync<List<AvailableGenreResponse>>(JsonOptions, TestContext.Current.CancellationToken);
|
||||
Assert.Equal(Enum.GetValues<Genre>().Length, body!.Count);
|
||||
Assert.Contains(Genre.Lofi, body.Single(g => g.Genre == Genre.Metal).IncompatibleWith);
|
||||
}
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var body = await response.Content.ReadFromJsonAsync<List<AvailableGenreResponse>>(JsonOptions, TestContext.Current.CancellationToken);
|
||||
Assert.Equal(Enum.GetValues<Genre>().Length, body!.Count);
|
||||
Assert.Contains(Genre.Lofi, body.Single(g => g.Genre == Genre.Metal).IncompatibleWith);
|
||||
}
|
||||
[Theory]
|
||||
[InlineData("Nope")]
|
||||
[InlineData("999")]
|
||||
public async Task GetTracks_UnknownGenre_ReturnsBadRequest(string genre)
|
||||
{
|
||||
var response = await fixture.CreateAnonymousClient().GetAsync($"/tracks?genre={genre}", TestContext.Current.CancellationToken);
|
||||
|
||||
[Theory]
|
||||
[InlineData("Nope")]
|
||||
[InlineData("999")]
|
||||
public async Task GetTracks_UnknownGenre_ReturnsBadRequest(string genre)
|
||||
{
|
||||
var response = await fixture.CreateAnonymousClient().GetAsync($"/tracks?genre={genre}", TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
[Fact]
|
||||
public async Task GetTracks_GenreQuery_IsCaseInsensitive()
|
||||
{
|
||||
var title = await SeedTrackAsync(Genre.Blues);
|
||||
|
||||
[Fact]
|
||||
public async Task GetTracks_GenreQuery_IsCaseInsensitive()
|
||||
{
|
||||
var title = await SeedTrackAsync(Genre.Blues);
|
||||
var response = await fixture.CreateAnonymousClient().GetAsync("/tracks?genre=blues&pageSize=100", TestContext.Current.CancellationToken);
|
||||
|
||||
var response = await fixture.CreateAnonymousClient().GetAsync("/tracks?genre=blues&pageSize=100", TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var body = await response.Content.ReadFromJsonAsync<TracksSearchResponse>(JsonOptions, TestContext.Current.CancellationToken);
|
||||
Assert.Contains(body!.Items, item => item.Track.Title == title);
|
||||
}
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var body = await response.Content.ReadFromJsonAsync<TracksSearchResponse>(JsonOptions, TestContext.Current.CancellationToken);
|
||||
Assert.Contains(body!.Items, item => item.Track.Title == title);
|
||||
}
|
||||
[Fact]
|
||||
public async Task GetTracks_GenreQuery_FiltersByTag()
|
||||
{
|
||||
var title = await SeedTrackAsync(Genre.Ambient);
|
||||
await SeedTrackAsync(Genre.Techno);
|
||||
|
||||
[Fact]
|
||||
public async Task GetTracks_GenreQuery_FiltersByTag()
|
||||
{
|
||||
var title = await SeedTrackAsync(Genre.Ambient);
|
||||
await SeedTrackAsync(Genre.Techno);
|
||||
var response = await fixture.CreateAnonymousClient().GetAsync("/tracks?genre=Ambient&pageSize=100", TestContext.Current.CancellationToken);
|
||||
|
||||
var response = await fixture.CreateAnonymousClient().GetAsync("/tracks?genre=Ambient&pageSize=100", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var body = await response.Content.ReadFromJsonAsync<TracksSearchResponse>(JsonOptions, TestContext.Current.CancellationToken);
|
||||
Assert.Contains(body!.Items, item => item.Track.Title == title);
|
||||
Assert.All(body.Items, item => Assert.Contains(Genre.Ambient, item.Track.Tags));
|
||||
}
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var body = await response.Content.ReadFromJsonAsync<TracksSearchResponse>(JsonOptions, TestContext.Current.CancellationToken);
|
||||
Assert.Contains(body!.Items, item => item.Track.Title == title);
|
||||
Assert.All(body.Items, item => Assert.Contains(Genre.Ambient, item.Track.Tags));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,106 +4,105 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Musify.Api.DataTransferObjects.Tracks;
|
||||
using Musify.Api.DataTransferObjects.Users;
|
||||
using Musify.Api.Tests.TestSupport;
|
||||
using Musify.Domain.Entities;
|
||||
using Musify.Infrastructure.Persistence;
|
||||
using Xunit;
|
||||
using Musify.Api.Tests.TestSupport;
|
||||
|
||||
namespace Musify.Api.Tests.Endpoints
|
||||
namespace Musify.Api.Tests.Endpoints;
|
||||
|
||||
[Collection(ApiCollection.Name)]
|
||||
public sealed class ListeningProgressEndpointsTests(ApiTestFixture fixture)
|
||||
{
|
||||
[Collection(ApiCollection.Name)]
|
||||
public sealed class ListeningProgressEndpointsTests(ApiTestFixture fixture)
|
||||
private async Task<long> CreateUserAsync()
|
||||
{
|
||||
private async Task<long> CreateUserAsync()
|
||||
var userId = Random.Shared.NextInt64(1, long.MaxValue);
|
||||
var response = await fixture.CreateAnonymousClient().PostAsJsonAsync(
|
||||
"/users", new CreateUserRequest(userId, $"user-{userId}", null, null), TestContext.Current.CancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
return userId;
|
||||
}
|
||||
|
||||
private async Task<Guid> SeedListenAsync(long userId, double durationSeconds)
|
||||
{
|
||||
using var scope = fixture.Services.CreateScope();
|
||||
var database = scope.ServiceProvider.GetRequiredService<Database>();
|
||||
var owner = await database.Users.SingleAsync(u => u.Id == userId, TestContext.Current.CancellationToken);
|
||||
var track = new Track
|
||||
{
|
||||
var userId = Random.Shared.NextInt64(1, long.MaxValue);
|
||||
var response = await fixture.CreateAnonymousClient().PostAsJsonAsync(
|
||||
"/users", new CreateUserRequest(userId, $"user-{userId}", null, null), TestContext.Current.CancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
return userId;
|
||||
}
|
||||
|
||||
private async Task<Guid> SeedListenAsync(long userId, double durationSeconds)
|
||||
Title = "Progress track",
|
||||
NormalizedTitle = "PROGRESS TRACK",
|
||||
OwnerUserId = owner.Id,
|
||||
Owner = owner,
|
||||
DurationSeconds = durationSeconds
|
||||
};
|
||||
var listen = new ListeningHistory
|
||||
{
|
||||
using var scope = fixture.Services.CreateScope();
|
||||
var database = scope.ServiceProvider.GetRequiredService<Database>();
|
||||
var owner = await database.Users.SingleAsync(u => u.Id == userId, TestContext.Current.CancellationToken);
|
||||
var track = new Track
|
||||
{
|
||||
Title = "Progress track",
|
||||
NormalizedTitle = "PROGRESS TRACK",
|
||||
OwnerUserId = owner.Id,
|
||||
Owner = owner,
|
||||
DurationSeconds = durationSeconds
|
||||
};
|
||||
var listen = new ListeningHistory
|
||||
{
|
||||
UserId = userId,
|
||||
TrackId = track.Id,
|
||||
ListenedAt = DateTime.UtcNow.AddMinutes(-10)
|
||||
};
|
||||
database.Tracks.Add(track);
|
||||
database.ListeningHistories.Add(listen);
|
||||
await database.SaveChangesAsync(TestContext.Current.CancellationToken);
|
||||
return listen.Id;
|
||||
}
|
||||
UserId = userId,
|
||||
TrackId = track.Id,
|
||||
ListenedAt = DateTime.UtcNow.AddMinutes(-10)
|
||||
};
|
||||
database.Tracks.Add(track);
|
||||
database.ListeningHistories.Add(listen);
|
||||
await database.SaveChangesAsync(TestContext.Current.CancellationToken);
|
||||
return listen.Id;
|
||||
}
|
||||
|
||||
private async Task<ListeningHistory> LoadListenAsync(Guid listenId)
|
||||
{
|
||||
using var scope = fixture.Services.CreateScope();
|
||||
var database = scope.ServiceProvider.GetRequiredService<Database>();
|
||||
return await database.ListeningHistories.AsNoTracking()
|
||||
.SingleAsync(l => l.Id == listenId, TestContext.Current.CancellationToken);
|
||||
}
|
||||
private async Task<ListeningHistory> LoadListenAsync(Guid listenId)
|
||||
{
|
||||
using var scope = fixture.Services.CreateScope();
|
||||
var database = scope.ServiceProvider.GetRequiredService<Database>();
|
||||
return await database.ListeningHistories.AsNoTracking()
|
||||
.SingleAsync(l => l.Id == listenId, TestContext.Current.CancellationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PutProgress_Anonymous_ReturnsUnauthorized()
|
||||
{
|
||||
var response = await fixture.CreateAnonymousClient().PutAsJsonAsync(
|
||||
$"/tracks/listens/{Guid.NewGuid()}/progress", new RecordListeningProgressRequest(10), TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task PutProgress_Anonymous_ReturnsUnauthorized()
|
||||
{
|
||||
var response = await fixture.CreateAnonymousClient().PutAsJsonAsync(
|
||||
$"/tracks/listens/{Guid.NewGuid()}/progress", new RecordListeningProgressRequest(10), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||
}
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PutProgress_OwnListen_StoresSecondsAndMarksCounted()
|
||||
{
|
||||
var userId = await CreateUserAsync();
|
||||
var listenId = await SeedListenAsync(userId, durationSeconds: 200);
|
||||
[Fact]
|
||||
public async Task PutProgress_OwnListen_StoresSecondsAndMarksCounted()
|
||||
{
|
||||
var userId = await CreateUserAsync();
|
||||
var listenId = await SeedListenAsync(userId, durationSeconds: 200);
|
||||
|
||||
var response = await fixture.CreateAuthenticatedClient(userId).PutAsJsonAsync(
|
||||
$"/tracks/listens/{listenId}/progress", new RecordListeningProgressRequest(45), TestContext.Current.CancellationToken);
|
||||
var response = await fixture.CreateAuthenticatedClient(userId).PutAsJsonAsync(
|
||||
$"/tracks/listens/{listenId}/progress", new RecordListeningProgressRequest(45), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
|
||||
var stored = await LoadListenAsync(listenId);
|
||||
Assert.Equal(45, stored.PlayedSeconds);
|
||||
Assert.True(stored.IsCounted);
|
||||
}
|
||||
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
|
||||
var stored = await LoadListenAsync(listenId);
|
||||
Assert.Equal(45, stored.PlayedSeconds);
|
||||
Assert.True(stored.IsCounted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PutProgress_OtherUsersListen_ReturnsNotFound()
|
||||
{
|
||||
var ownerId = await CreateUserAsync();
|
||||
var otherId = await CreateUserAsync();
|
||||
var listenId = await SeedListenAsync(ownerId, durationSeconds: 200);
|
||||
[Fact]
|
||||
public async Task PutProgress_OtherUsersListen_ReturnsNotFound()
|
||||
{
|
||||
var ownerId = await CreateUserAsync();
|
||||
var otherId = await CreateUserAsync();
|
||||
var listenId = await SeedListenAsync(ownerId, durationSeconds: 200);
|
||||
|
||||
var response = await fixture.CreateAuthenticatedClient(otherId).PutAsJsonAsync(
|
||||
$"/tracks/listens/{listenId}/progress", new RecordListeningProgressRequest(45), TestContext.Current.CancellationToken);
|
||||
var response = await fixture.CreateAuthenticatedClient(otherId).PutAsJsonAsync(
|
||||
$"/tracks/listens/{listenId}/progress", new RecordListeningProgressRequest(45), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
Assert.Null((await LoadListenAsync(listenId)).PlayedSeconds);
|
||||
}
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
Assert.Null((await LoadListenAsync(listenId)).PlayedSeconds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PutProgress_NegativeSeconds_ReturnsBadRequest()
|
||||
{
|
||||
var userId = await CreateUserAsync();
|
||||
var listenId = await SeedListenAsync(userId, durationSeconds: 200);
|
||||
[Fact]
|
||||
public async Task PutProgress_NegativeSeconds_ReturnsBadRequest()
|
||||
{
|
||||
var userId = await CreateUserAsync();
|
||||
var listenId = await SeedListenAsync(userId, durationSeconds: 200);
|
||||
|
||||
var response = await fixture.CreateAuthenticatedClient(userId).PutAsJsonAsync(
|
||||
$"/tracks/listens/{listenId}/progress", new RecordListeningProgressRequest(-5), TestContext.Current.CancellationToken);
|
||||
var response = await fixture.CreateAuthenticatedClient(userId).PutAsJsonAsync(
|
||||
$"/tracks/listens/{listenId}/progress", new RecordListeningProgressRequest(-5), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,125 +12,124 @@ using Musify.Domain.Entities;
|
||||
using Musify.Infrastructure.Persistence;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Api.Tests.Endpoints
|
||||
namespace Musify.Api.Tests.Endpoints;
|
||||
|
||||
[Collection(ApiCollection.Name)]
|
||||
public sealed class UserEndpointsTests(ApiTestFixture fixture)
|
||||
{
|
||||
[Collection(ApiCollection.Name)]
|
||||
public sealed class UserEndpointsTests(ApiTestFixture fixture)
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
};
|
||||
|
||||
private async Task SeedListenAsync(long userId, string trackTitle, DateTime listenedAt)
|
||||
{
|
||||
using var scope = fixture.Services.CreateScope();
|
||||
var database = scope.ServiceProvider.GetRequiredService<Database>();
|
||||
var owner = await database.Users.SingleAsync(u => u.Id == userId);
|
||||
var track = new Track
|
||||
{
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
Title = trackTitle,
|
||||
NormalizedTitle = trackTitle.ToUpperInvariant(),
|
||||
OwnerUserId = owner.Id,
|
||||
Owner = owner
|
||||
};
|
||||
|
||||
private async Task SeedListenAsync(long userId, string trackTitle, DateTime listenedAt)
|
||||
database.Tracks.Add(track);
|
||||
database.ListeningHistories.Add(new ListeningHistory
|
||||
{
|
||||
using var scope = fixture.Services.CreateScope();
|
||||
var database = scope.ServiceProvider.GetRequiredService<Database>();
|
||||
var owner = await database.Users.SingleAsync(u => u.Id == userId);
|
||||
var track = new Track
|
||||
{
|
||||
Title = trackTitle,
|
||||
NormalizedTitle = trackTitle.ToUpperInvariant(),
|
||||
OwnerUserId = owner.Id,
|
||||
Owner = owner
|
||||
};
|
||||
database.Tracks.Add(track);
|
||||
database.ListeningHistories.Add(new ListeningHistory
|
||||
{
|
||||
UserId = userId,
|
||||
TrackId = track.Id,
|
||||
ListenedAt = listenedAt
|
||||
});
|
||||
await database.SaveChangesAsync(TestContext.Current.CancellationToken);
|
||||
}
|
||||
UserId = userId,
|
||||
TrackId = track.Id,
|
||||
ListenedAt = listenedAt
|
||||
});
|
||||
await database.SaveChangesAsync(TestContext.Current.CancellationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PostUsers_NewUser_ReturnsCreatedWithLocationHeader()
|
||||
{
|
||||
var client = fixture.CreateAnonymousClient();
|
||||
var userId = Random.Shared.NextInt64(1, long.MaxValue);
|
||||
[Fact]
|
||||
public async Task PostUsers_NewUser_ReturnsCreatedWithLocationHeader()
|
||||
{
|
||||
var client = fixture.CreateAnonymousClient();
|
||||
var userId = Random.Shared.NextInt64(1, long.MaxValue);
|
||||
|
||||
var response = await client.PostAsJsonAsync("/users", new CreateUserRequest(userId, "Jane Doe", "Jane", "Doe"), TestContext.Current.CancellationToken);
|
||||
var response = await client.PostAsJsonAsync("/users", new CreateUserRequest(userId, "Jane Doe", "Jane", "Doe"), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
|
||||
Assert.Equal($"/users/{userId}", response.Headers.Location?.OriginalString);
|
||||
var body = await response.Content.ReadFromJsonAsync<UserApplicationResponse>(TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(body);
|
||||
Assert.Equal("Jane Doe", body.Name);
|
||||
}
|
||||
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
|
||||
Assert.Equal($"/users/{userId}", response.Headers.Location?.OriginalString);
|
||||
var body = await response.Content.ReadFromJsonAsync<UserApplicationResponse>(TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(body);
|
||||
Assert.Equal("Jane Doe", body.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PostUsers_BlankName_ReturnsValidationProblem()
|
||||
{
|
||||
var client = fixture.CreateAnonymousClient();
|
||||
[Fact]
|
||||
public async Task PostUsers_BlankName_ReturnsValidationProblem()
|
||||
{
|
||||
var client = fixture.CreateAnonymousClient();
|
||||
|
||||
var response = await client.PostAsJsonAsync("/users", new CreateUserRequest(Random.Shared.NextInt64(1, long.MaxValue), "", null, null), TestContext.Current.CancellationToken);
|
||||
var response = await client.PostAsJsonAsync("/users", new CreateUserRequest(Random.Shared.NextInt64(1, long.MaxValue), "", null, null), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PostUsers_DuplicateId_ReturnsConflict()
|
||||
{
|
||||
var client = fixture.CreateAnonymousClient();
|
||||
var userId = Random.Shared.NextInt64(1, long.MaxValue);
|
||||
await client.PostAsJsonAsync("/users", new CreateUserRequest(userId, "First", null, null), TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task PostUsers_DuplicateId_ReturnsConflict()
|
||||
{
|
||||
var client = fixture.CreateAnonymousClient();
|
||||
var userId = Random.Shared.NextInt64(1, long.MaxValue);
|
||||
await client.PostAsJsonAsync("/users", new CreateUserRequest(userId, "First", null, null), TestContext.Current.CancellationToken);
|
||||
|
||||
var response = await client.PostAsJsonAsync("/users", new CreateUserRequest(userId, "Second", null, null), TestContext.Current.CancellationToken);
|
||||
var response = await client.PostAsJsonAsync("/users", new CreateUserRequest(userId, "Second", null, null), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.Conflict, response.StatusCode);
|
||||
}
|
||||
Assert.Equal(HttpStatusCode.Conflict, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetUserById_ExistingUser_ReturnsIt()
|
||||
{
|
||||
var client = fixture.CreateAnonymousClient();
|
||||
var userId = Random.Shared.NextInt64(1, long.MaxValue);
|
||||
await client.PostAsJsonAsync("/users", new CreateUserRequest(userId, "Findable User", null, null), TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task GetUserById_ExistingUser_ReturnsIt()
|
||||
{
|
||||
var client = fixture.CreateAnonymousClient();
|
||||
var userId = Random.Shared.NextInt64(1, long.MaxValue);
|
||||
await client.PostAsJsonAsync("/users", new CreateUserRequest(userId, "Findable User", null, null), TestContext.Current.CancellationToken);
|
||||
|
||||
var response = await client.GetAsync($"/users/{userId}", TestContext.Current.CancellationToken);
|
||||
var response = await client.GetAsync($"/users/{userId}", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var body = await response.Content.ReadFromJsonAsync<UserApplicationResponse>(TestContext.Current.CancellationToken);
|
||||
Assert.Equal("Findable User", body?.Name);
|
||||
}
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var body = await response.Content.ReadFromJsonAsync<UserApplicationResponse>(TestContext.Current.CancellationToken);
|
||||
Assert.Equal("Findable User", body?.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetUserById_MissingUser_ReturnsNotFound()
|
||||
{
|
||||
var client = fixture.CreateAnonymousClient();
|
||||
[Fact]
|
||||
public async Task GetUserById_MissingUser_ReturnsNotFound()
|
||||
{
|
||||
var client = fixture.CreateAnonymousClient();
|
||||
|
||||
var response = await client.GetAsync($"/users/{Random.Shared.NextInt64(1, long.MaxValue)}", TestContext.Current.CancellationToken);
|
||||
var response = await client.GetAsync($"/users/{Random.Shared.NextInt64(1, long.MaxValue)}", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetLastTrackListenedByUserId_HasHistory_ReturnsMostRecentTrack()
|
||||
{
|
||||
var client = fixture.CreateAnonymousClient();
|
||||
var userId = Random.Shared.NextInt64(1, long.MaxValue);
|
||||
await client.PostAsJsonAsync("/users", new CreateUserRequest(userId, $"user-{userId}", null, null), TestContext.Current.CancellationToken);
|
||||
await SeedListenAsync(userId, "Older listen", DateTime.UtcNow.AddMinutes(-10));
|
||||
await SeedListenAsync(userId, "Newer listen", DateTime.UtcNow);
|
||||
[Fact]
|
||||
public async Task GetLastTrackListenedByUserId_HasHistory_ReturnsMostRecentTrack()
|
||||
{
|
||||
var client = fixture.CreateAnonymousClient();
|
||||
var userId = Random.Shared.NextInt64(1, long.MaxValue);
|
||||
await client.PostAsJsonAsync("/users", new CreateUserRequest(userId, $"user-{userId}", null, null), TestContext.Current.CancellationToken);
|
||||
await SeedListenAsync(userId, "Older listen", DateTime.UtcNow.AddMinutes(-10));
|
||||
await SeedListenAsync(userId, "Newer listen", DateTime.UtcNow);
|
||||
|
||||
var response = await client.GetAsync($"/users/{userId}/last-listened-track", TestContext.Current.CancellationToken);
|
||||
var response = await client.GetAsync($"/users/{userId}/last-listened-track", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var body = await response.Content.ReadFromJsonAsync<TrackApplicationResponse>(JsonOptions, TestContext.Current.CancellationToken);
|
||||
Assert.Equal("Newer listen", body?.Title);
|
||||
}
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var body = await response.Content.ReadFromJsonAsync<TrackApplicationResponse>(JsonOptions, TestContext.Current.CancellationToken);
|
||||
Assert.Equal("Newer listen", body?.Title);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetLastTrackListenedByUserId_NoHistory_ReturnsNotFound()
|
||||
{
|
||||
var client = fixture.CreateAnonymousClient();
|
||||
var userId = Random.Shared.NextInt64(1, long.MaxValue);
|
||||
await client.PostAsJsonAsync("/users", new CreateUserRequest(userId, $"user-{userId}", null, null), TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task GetLastTrackListenedByUserId_NoHistory_ReturnsNotFound()
|
||||
{
|
||||
var client = fixture.CreateAnonymousClient();
|
||||
var userId = Random.Shared.NextInt64(1, long.MaxValue);
|
||||
await client.PostAsJsonAsync("/users", new CreateUserRequest(userId, $"user-{userId}", null, null), TestContext.Current.CancellationToken);
|
||||
|
||||
var response = await client.GetAsync($"/users/{userId}/last-listened-track", TestContext.Current.CancellationToken);
|
||||
var response = await client.GetAsync($"/users/{userId}/last-listened-track", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,133 +4,132 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Musify.Api.Extensions;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Api.Tests.Extensions
|
||||
namespace Musify.Api.Tests.Extensions;
|
||||
|
||||
public sealed class ErrorOrHttpExtensionsTests
|
||||
{
|
||||
public sealed class ErrorOrHttpExtensionsTests
|
||||
private static readonly IServiceProvider Services = new ServiceCollection().AddLogging().AddProblemDetails().BuildServiceProvider();
|
||||
|
||||
private static async Task<(int StatusCode, string Body)> ExecuteAsync(IResult result)
|
||||
{
|
||||
private static readonly IServiceProvider Services = new ServiceCollection().AddLogging().AddProblemDetails().BuildServiceProvider();
|
||||
var context = new DefaultHttpContext { RequestServices = Services };
|
||||
var bodyStream = new MemoryStream();
|
||||
context.Response.Body = bodyStream;
|
||||
|
||||
private static async Task<(int StatusCode, string Body)> ExecuteAsync(IResult result)
|
||||
await result.ExecuteAsync(context);
|
||||
|
||||
bodyStream.Position = 0;
|
||||
using var reader = new StreamReader(bodyStream);
|
||||
return (context.Response.StatusCode, await reader.ReadToEndAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ToHttpResult_Success_ReturnsNoContent()
|
||||
{
|
||||
ErrorOr<Success> result = new Success();
|
||||
|
||||
var (statusCode, _) = await ExecuteAsync(result.ToHttpResult());
|
||||
|
||||
Assert.Equal(StatusCodes.Status204NoContent, statusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ToHttpResult_Value_ReturnsOkWithTheValue()
|
||||
{
|
||||
ErrorOr<string> result = "hello";
|
||||
|
||||
var (statusCode, body) = await ExecuteAsync(result.ToHttpResult());
|
||||
|
||||
Assert.Equal(StatusCodes.Status200OK, statusCode);
|
||||
Assert.Contains("hello", body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ToHttpResult_NotFoundError_ReturnsNotFound()
|
||||
{
|
||||
ErrorOr<string> result = Error.NotFound(description: "not here");
|
||||
|
||||
var (statusCode, _) = await ExecuteAsync(result.ToHttpResult());
|
||||
|
||||
Assert.Equal(StatusCodes.Status404NotFound, statusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ToHttpResult_UnauthorizedError_ReturnsUnauthorized()
|
||||
{
|
||||
ErrorOr<string> result = Error.Unauthorized();
|
||||
|
||||
var (statusCode, _) = await ExecuteAsync(result.ToHttpResult());
|
||||
|
||||
Assert.Equal(StatusCodes.Status401Unauthorized, statusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ToHttpResult_ForbiddenError_ReturnsForbidden()
|
||||
{
|
||||
ErrorOr<string> result = Error.Forbidden();
|
||||
|
||||
var (statusCode, _) = await ExecuteAsync(result.ToHttpResult());
|
||||
|
||||
Assert.Equal(StatusCodes.Status403Forbidden, statusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ToHttpResult_ConflictError_ReturnsConflict()
|
||||
{
|
||||
ErrorOr<string> result = Error.Conflict();
|
||||
|
||||
var (statusCode, _) = await ExecuteAsync(result.ToHttpResult());
|
||||
|
||||
Assert.Equal(StatusCodes.Status409Conflict, statusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ToHttpResult_FailureError_ReturnsProblem500()
|
||||
{
|
||||
ErrorOr<string> result = Error.Failure();
|
||||
|
||||
var (statusCode, _) = await ExecuteAsync(result.ToHttpResult());
|
||||
|
||||
Assert.Equal(StatusCodes.Status500InternalServerError, statusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ToHttpResult_ValidationErrors_ReturnsValidationProblem()
|
||||
{
|
||||
ErrorOr<string> result = new List<Error>
|
||||
{
|
||||
var context = new DefaultHttpContext { RequestServices = Services };
|
||||
var bodyStream = new MemoryStream();
|
||||
context.Response.Body = bodyStream;
|
||||
Error.Validation("Name", "Name is required"),
|
||||
Error.Validation("Name", "Name is too long"),
|
||||
Error.Validation("Age", "Age must be positive"),
|
||||
};
|
||||
|
||||
await result.ExecuteAsync(context);
|
||||
var (statusCode, body) = await ExecuteAsync(result.ToHttpResult());
|
||||
|
||||
bodyStream.Position = 0;
|
||||
using var reader = new StreamReader(bodyStream);
|
||||
return (context.Response.StatusCode, await reader.ReadToEndAsync());
|
||||
}
|
||||
Assert.Equal(StatusCodes.Status400BadRequest, statusCode);
|
||||
Assert.Contains("Name is required", body);
|
||||
Assert.Contains("Age must be positive", body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ToHttpResult_Success_ReturnsNoContent()
|
||||
{
|
||||
ErrorOr<Success> result = new Success();
|
||||
[Fact]
|
||||
public async Task ToCreatedResult_Success_ReturnsCreatedAtTheGivenLocation()
|
||||
{
|
||||
ErrorOr<string> result = "the-id";
|
||||
|
||||
var (statusCode, _) = await ExecuteAsync(result.ToHttpResult());
|
||||
var context = new DefaultHttpContext { RequestServices = Services, Response = { Body = new MemoryStream() } };
|
||||
await result.ToCreatedResult(value => $"/things/{value}").ExecuteAsync(context);
|
||||
|
||||
Assert.Equal(StatusCodes.Status204NoContent, statusCode);
|
||||
}
|
||||
Assert.Equal(StatusCodes.Status201Created, context.Response.StatusCode);
|
||||
Assert.Equal("/things/the-id", context.Response.Headers.Location);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ToHttpResult_Value_ReturnsOkWithTheValue()
|
||||
{
|
||||
ErrorOr<string> result = "hello";
|
||||
[Fact]
|
||||
public async Task ToCreatedResult_Error_ReturnsTheMappedErrorStatus()
|
||||
{
|
||||
ErrorOr<string> result = Error.Conflict();
|
||||
|
||||
var (statusCode, body) = await ExecuteAsync(result.ToHttpResult());
|
||||
var (statusCode, _) = await ExecuteAsync(result.ToCreatedResult(value => $"/things/{value}"));
|
||||
|
||||
Assert.Equal(StatusCodes.Status200OK, statusCode);
|
||||
Assert.Contains("hello", body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ToHttpResult_NotFoundError_ReturnsNotFound()
|
||||
{
|
||||
ErrorOr<string> result = Error.NotFound(description: "not here");
|
||||
|
||||
var (statusCode, _) = await ExecuteAsync(result.ToHttpResult());
|
||||
|
||||
Assert.Equal(StatusCodes.Status404NotFound, statusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ToHttpResult_UnauthorizedError_ReturnsUnauthorized()
|
||||
{
|
||||
ErrorOr<string> result = Error.Unauthorized();
|
||||
|
||||
var (statusCode, _) = await ExecuteAsync(result.ToHttpResult());
|
||||
|
||||
Assert.Equal(StatusCodes.Status401Unauthorized, statusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ToHttpResult_ForbiddenError_ReturnsForbidden()
|
||||
{
|
||||
ErrorOr<string> result = Error.Forbidden();
|
||||
|
||||
var (statusCode, _) = await ExecuteAsync(result.ToHttpResult());
|
||||
|
||||
Assert.Equal(StatusCodes.Status403Forbidden, statusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ToHttpResult_ConflictError_ReturnsConflict()
|
||||
{
|
||||
ErrorOr<string> result = Error.Conflict();
|
||||
|
||||
var (statusCode, _) = await ExecuteAsync(result.ToHttpResult());
|
||||
|
||||
Assert.Equal(StatusCodes.Status409Conflict, statusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ToHttpResult_FailureError_ReturnsProblem500()
|
||||
{
|
||||
ErrorOr<string> result = Error.Failure();
|
||||
|
||||
var (statusCode, _) = await ExecuteAsync(result.ToHttpResult());
|
||||
|
||||
Assert.Equal(StatusCodes.Status500InternalServerError, statusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ToHttpResult_ValidationErrors_ReturnsValidationProblem()
|
||||
{
|
||||
ErrorOr<string> result = new List<Error>
|
||||
{
|
||||
Error.Validation("Name", "Name is required"),
|
||||
Error.Validation("Name", "Name is too long"),
|
||||
Error.Validation("Age", "Age must be positive"),
|
||||
};
|
||||
|
||||
var (statusCode, body) = await ExecuteAsync(result.ToHttpResult());
|
||||
|
||||
Assert.Equal(StatusCodes.Status400BadRequest, statusCode);
|
||||
Assert.Contains("Name is required", body);
|
||||
Assert.Contains("Age must be positive", body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ToCreatedResult_Success_ReturnsCreatedAtTheGivenLocation()
|
||||
{
|
||||
ErrorOr<string> result = "the-id";
|
||||
|
||||
var context = new DefaultHttpContext { RequestServices = Services, Response = { Body = new MemoryStream() } };
|
||||
await result.ToCreatedResult(value => $"/things/{value}").ExecuteAsync(context);
|
||||
|
||||
Assert.Equal(StatusCodes.Status201Created, context.Response.StatusCode);
|
||||
Assert.Equal("/things/the-id", context.Response.Headers.Location);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ToCreatedResult_Error_ReturnsTheMappedErrorStatus()
|
||||
{
|
||||
ErrorOr<string> result = Error.Conflict();
|
||||
|
||||
var (statusCode, _) = await ExecuteAsync(result.ToCreatedResult(value => $"/things/{value}"));
|
||||
|
||||
Assert.Equal(StatusCodes.Status409Conflict, statusCode);
|
||||
}
|
||||
Assert.Equal(StatusCodes.Status409Conflict, statusCode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,57 +6,56 @@ using Musify.Api.Filters;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Api.Tests.Filters
|
||||
namespace Musify.Api.Tests.Filters;
|
||||
|
||||
public sealed class ValidationFilterTests
|
||||
{
|
||||
public sealed class ValidationFilterTests
|
||||
public sealed record Payload(string Name);
|
||||
|
||||
private static EndpointFilterInvocationContext CreateContext<T>(T argument) =>
|
||||
EndpointFilterInvocationContext.Create(new DefaultHttpContext(), argument);
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAsync_ValidPayload_CallsNext()
|
||||
{
|
||||
public sealed record Payload(string Name);
|
||||
var validator = Substitute.For<IValidator<Payload>>();
|
||||
validator.ValidateAsync(Arg.Any<Payload>(), Arg.Any<CancellationToken>()).Returns(new ValidationResult());
|
||||
var filter = new ValidationFilter<Payload>(validator);
|
||||
var context = CreateContext(new Payload("ok"));
|
||||
|
||||
private static EndpointFilterInvocationContext CreateContext<T>(T argument) =>
|
||||
EndpointFilterInvocationContext.Create(new DefaultHttpContext(), argument);
|
||||
var result = await filter.InvokeAsync(context, _ => ValueTask.FromResult<object?>("next-called"));
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAsync_ValidPayload_CallsNext()
|
||||
{
|
||||
var validator = Substitute.For<IValidator<Payload>>();
|
||||
validator.ValidateAsync(Arg.Any<Payload>(), Arg.Any<CancellationToken>()).Returns(new ValidationResult());
|
||||
var filter = new ValidationFilter<Payload>(validator);
|
||||
var context = CreateContext(new Payload("ok"));
|
||||
Assert.Equal("next-called", result);
|
||||
await validator.Received(1).ValidateAsync(Arg.Is<Payload>(p => p.Name == "ok"), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
var result = await filter.InvokeAsync(context, _ => ValueTask.FromResult<object?>("next-called"));
|
||||
[Fact]
|
||||
public async Task InvokeAsync_InvalidPayload_ReturnsValidationProblemWithoutCallingNext()
|
||||
{
|
||||
var validator = Substitute.For<IValidator<Payload>>();
|
||||
validator.ValidateAsync(Arg.Any<Payload>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new ValidationResult([new ValidationFailure("Name", "Name is required")]));
|
||||
var filter = new ValidationFilter<Payload>(validator);
|
||||
var context = CreateContext(new Payload(""));
|
||||
var nextCalled = false;
|
||||
|
||||
Assert.Equal("next-called", result);
|
||||
await validator.Received(1).ValidateAsync(Arg.Is<Payload>(p => p.Name == "ok"), Arg.Any<CancellationToken>());
|
||||
}
|
||||
var result = await filter.InvokeAsync(context, _ => { nextCalled = true; return ValueTask.FromResult<object?>(null); });
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAsync_InvalidPayload_ReturnsValidationProblemWithoutCallingNext()
|
||||
{
|
||||
var validator = Substitute.For<IValidator<Payload>>();
|
||||
validator.ValidateAsync(Arg.Any<Payload>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new ValidationResult([new ValidationFailure("Name", "Name is required")]));
|
||||
var filter = new ValidationFilter<Payload>(validator);
|
||||
var context = CreateContext(new Payload(""));
|
||||
var nextCalled = false;
|
||||
Assert.False(nextCalled);
|
||||
var problem = Assert.IsType<ProblemHttpResult>(result);
|
||||
Assert.Equal(StatusCodes.Status400BadRequest, problem.StatusCode);
|
||||
}
|
||||
|
||||
var result = await filter.InvokeAsync(context, _ => { nextCalled = true; return ValueTask.FromResult<object?>(null); });
|
||||
[Fact]
|
||||
public async Task InvokeAsync_NoMatchingArgument_CallsNextWithoutValidating()
|
||||
{
|
||||
var validator = Substitute.For<IValidator<Payload>>();
|
||||
var filter = new ValidationFilter<Payload>(validator);
|
||||
var context = CreateContext(42);
|
||||
|
||||
Assert.False(nextCalled);
|
||||
var problem = Assert.IsType<ProblemHttpResult>(result);
|
||||
Assert.Equal(StatusCodes.Status400BadRequest, problem.StatusCode);
|
||||
}
|
||||
var result = await filter.InvokeAsync(context, _ => ValueTask.FromResult<object?>("next-called"));
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAsync_NoMatchingArgument_CallsNextWithoutValidating()
|
||||
{
|
||||
var validator = Substitute.For<IValidator<Payload>>();
|
||||
var filter = new ValidationFilter<Payload>(validator);
|
||||
var context = CreateContext(42);
|
||||
|
||||
var result = await filter.InvokeAsync(context, _ => ValueTask.FromResult<object?>("next-called"));
|
||||
|
||||
Assert.Equal("next-called", result);
|
||||
await validator.DidNotReceiveWithAnyArgs().ValidateAsync(Arg.Any<Payload>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
Assert.Equal("next-called", result);
|
||||
await validator.DidNotReceiveWithAnyArgs().ValidateAsync(Arg.Any<Payload>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,92 +11,91 @@ using NSubstitute;
|
||||
using Testcontainers.PostgreSql;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Api.Tests.TestSupport
|
||||
namespace Musify.Api.Tests.TestSupport;
|
||||
|
||||
public sealed class ApiTestFixture : WebApplicationFactory<Program>, IAsyncLifetime
|
||||
{
|
||||
public sealed class ApiTestFixture : WebApplicationFactory<Program>, IAsyncLifetime
|
||||
private readonly PostgreSqlContainer postgres = new PostgreSqlBuilder("postgres:17-alpine")
|
||||
.WithDatabase("musify_api_test")
|
||||
.WithUsername("postgres")
|
||||
.WithPassword("postgres")
|
||||
.Build();
|
||||
|
||||
public IStorageService StorageService { get; } = Substitute.For<IStorageService>();
|
||||
|
||||
public IEventBus EventBus { get; } = Substitute.For<IEventBus>();
|
||||
|
||||
public IStreamTicketService StreamTicketService { get; } = Substitute.For<IStreamTicketService>();
|
||||
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
private readonly PostgreSqlContainer postgres = new PostgreSqlBuilder("postgres:17-alpine")
|
||||
.WithDatabase("musify_api_test")
|
||||
.WithUsername("postgres")
|
||||
.WithPassword("postgres")
|
||||
.Build();
|
||||
await postgres.StartAsync();
|
||||
|
||||
public IStorageService StorageService { get; } = Substitute.For<IStorageService>();
|
||||
|
||||
public IEventBus EventBus { get; } = Substitute.For<IEventBus>();
|
||||
|
||||
public IStreamTicketService StreamTicketService { get; } = Substitute.For<IStreamTicketService>();
|
||||
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
await postgres.StartAsync();
|
||||
|
||||
using var scope = Services.CreateScope();
|
||||
var database = scope.ServiceProvider.GetRequiredService<Musify.Infrastructure.Persistence.Database>();
|
||||
await database.Database.MigrateAsync();
|
||||
}
|
||||
|
||||
public override async ValueTask DisposeAsync()
|
||||
{
|
||||
await base.DisposeAsync();
|
||||
await postgres.DisposeAsync();
|
||||
}
|
||||
|
||||
public HttpClient CreateAnonymousClient() => CreateClient();
|
||||
|
||||
public HttpClient CreateAuthenticatedClient(long userId, string userName = "test-user")
|
||||
{
|
||||
var client = CreateClient();
|
||||
client.DefaultRequestHeaders.Add(FakeAuthenticationHandler.UserIdHeader, userId.ToString());
|
||||
client.DefaultRequestHeaders.Add("X-Test-User-Name", userName);
|
||||
return client;
|
||||
}
|
||||
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
{
|
||||
builder.ConfigureAppConfiguration((_, configuration) =>
|
||||
{
|
||||
configuration.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["Database:ConnectionString"] = postgres.GetConnectionString(),
|
||||
["Authentication:MetadataAddress"] = "https://auth.musify.test/.well-known/openid-configuration",
|
||||
["Authentication:IssuerAddress"] = "https://auth.musify.test",
|
||||
["Authentication:AudienceAddress"] = "musify-api-test",
|
||||
["Authentication:ClientId"] = "musify-api-test",
|
||||
["Authentication:AuthorizationEndpoint"] = "https://auth.musify.test/authorize",
|
||||
["Authentication:TokenEndpoint"] = "https://auth.musify.test/token",
|
||||
["Authentication:Scopes:0"] = "openid",
|
||||
["StreamTicket:PrivateKeyPath"] = "unused-in-tests.pem",
|
||||
["OpenTelemetry:OtlpEndpoint"] = "http://localhost:4317",
|
||||
["MassTransit:Address"] = "rabbitmq://localhost:5672",
|
||||
["MassTransit:Username"] = "guest",
|
||||
["MassTransit:Password"] = "guest",
|
||||
["InfrastructureStorage:Address"] = "http://localhost:8333",
|
||||
["InfrastructureStorage:AccessKey"] = "unused-in-tests",
|
||||
["InfrastructureStorage:SecretAccessKey"] = "unused-in-tests",
|
||||
["InfrastructureStorage:ForcePathStyle"] = "true",
|
||||
["InfrastructureStorage:UseHttp"] = "true",
|
||||
["ApplicationStorage:Bucket"] = "musify-api-test",
|
||||
["StreamGateway:PublicBaseUrl"] = "http://stream.musify.test",
|
||||
});
|
||||
});
|
||||
|
||||
builder.ConfigureTestServices(services =>
|
||||
{
|
||||
services.Replace(ServiceDescriptor.Singleton(StorageService));
|
||||
services.Replace(ServiceDescriptor.Scoped(_ => EventBus));
|
||||
services.Replace(ServiceDescriptor.Singleton(StreamTicketService));
|
||||
|
||||
services
|
||||
.AddAuthentication(FakeAuthenticationHandler.SchemeName)
|
||||
.AddScheme<AuthenticationSchemeOptions, FakeAuthenticationHandler>(FakeAuthenticationHandler.SchemeName, _ => { });
|
||||
});
|
||||
}
|
||||
using var scope = Services.CreateScope();
|
||||
var database = scope.ServiceProvider.GetRequiredService<Musify.Infrastructure.Persistence.Database>();
|
||||
await database.Database.MigrateAsync();
|
||||
}
|
||||
|
||||
[CollectionDefinition(Name)]
|
||||
public sealed class ApiCollection : ICollectionFixture<ApiTestFixture>
|
||||
public override async ValueTask DisposeAsync()
|
||||
{
|
||||
public const string Name = "Api";
|
||||
await base.DisposeAsync();
|
||||
await postgres.DisposeAsync();
|
||||
}
|
||||
|
||||
public HttpClient CreateAnonymousClient() => CreateClient();
|
||||
|
||||
public HttpClient CreateAuthenticatedClient(long userId, string userName = "test-user")
|
||||
{
|
||||
var client = CreateClient();
|
||||
client.DefaultRequestHeaders.Add(FakeAuthenticationHandler.UserIdHeader, userId.ToString());
|
||||
client.DefaultRequestHeaders.Add("X-Test-User-Name", userName);
|
||||
return client;
|
||||
}
|
||||
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
{
|
||||
builder.ConfigureAppConfiguration((_, configuration) =>
|
||||
{
|
||||
configuration.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["Database:ConnectionString"] = postgres.GetConnectionString(),
|
||||
["Authentication:MetadataAddress"] = "https://auth.musify.test/.well-known/openid-configuration",
|
||||
["Authentication:IssuerAddress"] = "https://auth.musify.test",
|
||||
["Authentication:AudienceAddress"] = "musify-api-test",
|
||||
["Authentication:ClientId"] = "musify-api-test",
|
||||
["Authentication:AuthorizationEndpoint"] = "https://auth.musify.test/authorize",
|
||||
["Authentication:TokenEndpoint"] = "https://auth.musify.test/token",
|
||||
["Authentication:Scopes:0"] = "openid",
|
||||
["StreamTicket:PrivateKeyPath"] = "unused-in-tests.pem",
|
||||
["OpenTelemetry:OtlpEndpoint"] = "http://localhost:4317",
|
||||
["MassTransit:Address"] = "rabbitmq://localhost:5672",
|
||||
["MassTransit:Username"] = "guest",
|
||||
["MassTransit:Password"] = "guest",
|
||||
["InfrastructureStorage:Address"] = "http://localhost:8333",
|
||||
["InfrastructureStorage:AccessKey"] = "unused-in-tests",
|
||||
["InfrastructureStorage:SecretAccessKey"] = "unused-in-tests",
|
||||
["InfrastructureStorage:ForcePathStyle"] = "true",
|
||||
["InfrastructureStorage:UseHttp"] = "true",
|
||||
["ApplicationStorage:Bucket"] = "musify-api-test",
|
||||
["StreamGateway:PublicBaseUrl"] = "http://stream.musify.test",
|
||||
});
|
||||
});
|
||||
|
||||
builder.ConfigureTestServices(services =>
|
||||
{
|
||||
services.Replace(ServiceDescriptor.Singleton(StorageService));
|
||||
services.Replace(ServiceDescriptor.Scoped(_ => EventBus));
|
||||
services.Replace(ServiceDescriptor.Singleton(StreamTicketService));
|
||||
|
||||
services
|
||||
.AddAuthentication(FakeAuthenticationHandler.SchemeName)
|
||||
.AddScheme<AuthenticationSchemeOptions, FakeAuthenticationHandler>(FakeAuthenticationHandler.SchemeName, _ => { });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[CollectionDefinition(Name)]
|
||||
public sealed class ApiCollection : ICollectionFixture<ApiTestFixture>
|
||||
{
|
||||
public const string Name = "Api";
|
||||
}
|
||||
|
||||
@@ -4,29 +4,28 @@ using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Musify.Api.Tests.TestSupport
|
||||
namespace Musify.Api.Tests.TestSupport;
|
||||
|
||||
public sealed class FakeAuthenticationHandler(
|
||||
IOptionsMonitor<AuthenticationSchemeOptions> options,
|
||||
ILoggerFactory logger,
|
||||
UrlEncoder encoder)
|
||||
: AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder)
|
||||
{
|
||||
public sealed class FakeAuthenticationHandler(
|
||||
IOptionsMonitor<AuthenticationSchemeOptions> options,
|
||||
ILoggerFactory logger,
|
||||
UrlEncoder encoder)
|
||||
: AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder)
|
||||
public const string SchemeName = "Test";
|
||||
public const string UserIdHeader = "X-Test-User-Id";
|
||||
|
||||
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
{
|
||||
public const string SchemeName = "Test";
|
||||
public const string UserIdHeader = "X-Test-User-Id";
|
||||
if (!Request.Headers.TryGetValue(UserIdHeader, out var userId) || string.IsNullOrEmpty(userId))
|
||||
return Task.FromResult(AuthenticateResult.NoResult());
|
||||
|
||||
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
{
|
||||
if (!Request.Headers.TryGetValue(UserIdHeader, out var userId) || string.IsNullOrEmpty(userId))
|
||||
return Task.FromResult(AuthenticateResult.NoResult());
|
||||
var claims = new List<Claim> { new(ClaimTypes.NameIdentifier, userId!) };
|
||||
if (Request.Headers.TryGetValue("X-Test-User-Name", out var userName) && !string.IsNullOrEmpty(userName))
|
||||
claims.Add(new Claim(ClaimTypes.Name, userName!));
|
||||
|
||||
var claims = new List<Claim> { new(ClaimTypes.NameIdentifier, userId!) };
|
||||
if (Request.Headers.TryGetValue("X-Test-User-Name", out var userName) && !string.IsNullOrEmpty(userName))
|
||||
claims.Add(new Claim(ClaimTypes.Name, userName!));
|
||||
|
||||
var identity = new ClaimsIdentity(claims, SchemeName);
|
||||
var ticket = new AuthenticationTicket(new ClaimsPrincipal(identity), SchemeName);
|
||||
return Task.FromResult(AuthenticateResult.Success(ticket));
|
||||
}
|
||||
var identity = new ClaimsIdentity(claims, SchemeName);
|
||||
var ticket = new AuthenticationTicket(new ClaimsPrincipal(identity), SchemeName);
|
||||
return Task.FromResult(AuthenticateResult.Success(ticket));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,84 +2,83 @@ using Musify.Api.DataTransferObjects.Albums;
|
||||
using Musify.Api.Validators.Albums;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Api.Tests.Validators
|
||||
namespace Musify.Api.Tests.Validators;
|
||||
|
||||
public sealed class CreateAlbumRequestValidatorTests
|
||||
{
|
||||
public sealed class CreateAlbumRequestValidatorTests
|
||||
private readonly CreateAlbumRequestValidator validator = new();
|
||||
|
||||
[Fact]
|
||||
public void Validate_ValidRequest_Passes()
|
||||
{
|
||||
private readonly CreateAlbumRequestValidator validator = new();
|
||||
var result = validator.Validate(new CreateAlbumRequest("My Album", "A short description", 2024, Guid.NewGuid()));
|
||||
|
||||
[Fact]
|
||||
public void Validate_ValidRequest_Passes()
|
||||
{
|
||||
var result = validator.Validate(new CreateAlbumRequest("My Album", "A short description", 2024, Guid.NewGuid()));
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
[Fact]
|
||||
public void Validate_ValidRequestWithoutOptionalFields_Passes()
|
||||
{
|
||||
var result = validator.Validate(new CreateAlbumRequest("My Album", null, null, Guid.NewGuid()));
|
||||
|
||||
[Fact]
|
||||
public void Validate_ValidRequestWithoutOptionalFields_Passes()
|
||||
{
|
||||
var result = validator.Validate(new CreateAlbumRequest("My Album", null, null, Guid.NewGuid()));
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
[Fact]
|
||||
public void Validate_BlankTitle_Fails()
|
||||
{
|
||||
var result = validator.Validate(new CreateAlbumRequest("", null, null, Guid.NewGuid()));
|
||||
|
||||
[Fact]
|
||||
public void Validate_BlankTitle_Fails()
|
||||
{
|
||||
var result = validator.Validate(new CreateAlbumRequest("", null, null, Guid.NewGuid()));
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Contains(result.Errors, e => e.PropertyName == nameof(CreateAlbumRequest.Title));
|
||||
}
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Contains(result.Errors, e => e.PropertyName == nameof(CreateAlbumRequest.Title));
|
||||
}
|
||||
[Fact]
|
||||
public void Validate_TitleOverMaxLength_Fails()
|
||||
{
|
||||
var result = validator.Validate(new CreateAlbumRequest(new string('a', 201), null, null, Guid.NewGuid()));
|
||||
|
||||
[Fact]
|
||||
public void Validate_TitleOverMaxLength_Fails()
|
||||
{
|
||||
var result = validator.Validate(new CreateAlbumRequest(new string('a', 201), null, null, Guid.NewGuid()));
|
||||
Assert.False(result.IsValid);
|
||||
}
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
}
|
||||
[Fact]
|
||||
public void Validate_ReleaseYearBeforeTheEarliestBoundary_Fails()
|
||||
{
|
||||
var result = validator.Validate(new CreateAlbumRequest("Title", null, AlbumReleaseYear.Earliest - 1, Guid.NewGuid()));
|
||||
|
||||
[Fact]
|
||||
public void Validate_ReleaseYearBeforeTheEarliestBoundary_Fails()
|
||||
{
|
||||
var result = validator.Validate(new CreateAlbumRequest("Title", null, AlbumReleaseYear.Earliest - 1, Guid.NewGuid()));
|
||||
Assert.False(result.IsValid);
|
||||
}
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
}
|
||||
[Fact]
|
||||
public void Validate_ReleaseYearAtTheEarliestBoundary_Passes()
|
||||
{
|
||||
var result = validator.Validate(new CreateAlbumRequest("Title", null, AlbumReleaseYear.Earliest, Guid.NewGuid()));
|
||||
|
||||
[Fact]
|
||||
public void Validate_ReleaseYearAtTheEarliestBoundary_Passes()
|
||||
{
|
||||
var result = validator.Validate(new CreateAlbumRequest("Title", null, AlbumReleaseYear.Earliest, Guid.NewGuid()));
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
[Fact]
|
||||
public void Validate_ReleaseYearAtTheLatestBoundary_Passes()
|
||||
{
|
||||
var result = validator.Validate(new CreateAlbumRequest("Title", null, AlbumReleaseYear.Latest, Guid.NewGuid()));
|
||||
|
||||
[Fact]
|
||||
public void Validate_ReleaseYearAtTheLatestBoundary_Passes()
|
||||
{
|
||||
var result = validator.Validate(new CreateAlbumRequest("Title", null, AlbumReleaseYear.Latest, Guid.NewGuid()));
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
[Fact]
|
||||
public void Validate_ReleaseYearPastTheLatestBoundary_Fails()
|
||||
{
|
||||
var result = validator.Validate(new CreateAlbumRequest("Title", null, AlbumReleaseYear.Latest + 1, Guid.NewGuid()));
|
||||
|
||||
[Fact]
|
||||
public void Validate_ReleaseYearPastTheLatestBoundary_Fails()
|
||||
{
|
||||
var result = validator.Validate(new CreateAlbumRequest("Title", null, AlbumReleaseYear.Latest + 1, Guid.NewGuid()));
|
||||
Assert.False(result.IsValid);
|
||||
}
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
}
|
||||
[Fact]
|
||||
public void Validate_EmptyPictureIntentId_Fails()
|
||||
{
|
||||
var result = validator.Validate(new CreateAlbumRequest("Title", null, null, Guid.Empty));
|
||||
|
||||
[Fact]
|
||||
public void Validate_EmptyPictureIntentId_Fails()
|
||||
{
|
||||
var result = validator.Validate(new CreateAlbumRequest("Title", null, null, Guid.Empty));
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Contains(result.Errors, e => e.PropertyName == nameof(CreateAlbumRequest.PictureIntentId));
|
||||
}
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Contains(result.Errors, e => e.PropertyName == nameof(CreateAlbumRequest.PictureIntentId));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,51 +2,50 @@ using Musify.Api.DataTransferObjects.Users;
|
||||
using Musify.Api.Validators.Users;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Api.Tests.Validators
|
||||
namespace Musify.Api.Tests.Validators;
|
||||
|
||||
public sealed class CreateUserRequestValidatorTests
|
||||
{
|
||||
public sealed class CreateUserRequestValidatorTests
|
||||
private readonly CreateUserRequestValidator validator = new();
|
||||
|
||||
[Fact]
|
||||
public void Validate_ValidRequest_Passes()
|
||||
{
|
||||
private readonly CreateUserRequestValidator validator = new();
|
||||
var result = validator.Validate(new CreateUserRequest(1, "Jane Doe", "Jane", "Doe"));
|
||||
|
||||
[Fact]
|
||||
public void Validate_ValidRequest_Passes()
|
||||
{
|
||||
var result = validator.Validate(new CreateUserRequest(1, "Jane Doe", "Jane", "Doe"));
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
[Fact]
|
||||
public void Validate_ZeroId_Fails()
|
||||
{
|
||||
var result = validator.Validate(new CreateUserRequest(0, "Jane Doe", null, null));
|
||||
|
||||
[Fact]
|
||||
public void Validate_ZeroId_Fails()
|
||||
{
|
||||
var result = validator.Validate(new CreateUserRequest(0, "Jane Doe", null, null));
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Contains(result.Errors, e => e.PropertyName == nameof(CreateUserRequest.Id));
|
||||
}
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Contains(result.Errors, e => e.PropertyName == nameof(CreateUserRequest.Id));
|
||||
}
|
||||
[Fact]
|
||||
public void Validate_BlankName_Fails()
|
||||
{
|
||||
var result = validator.Validate(new CreateUserRequest(1, "", null, null));
|
||||
|
||||
[Fact]
|
||||
public void Validate_BlankName_Fails()
|
||||
{
|
||||
var result = validator.Validate(new CreateUserRequest(1, "", null, null));
|
||||
Assert.False(result.IsValid);
|
||||
}
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
}
|
||||
[Fact]
|
||||
public void Validate_NameOverMaxLength_Fails()
|
||||
{
|
||||
var result = validator.Validate(new CreateUserRequest(1, new string('a', 49), null, null));
|
||||
|
||||
[Fact]
|
||||
public void Validate_NameOverMaxLength_Fails()
|
||||
{
|
||||
var result = validator.Validate(new CreateUserRequest(1, new string('a', 49), null, null));
|
||||
Assert.False(result.IsValid);
|
||||
}
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
}
|
||||
[Fact]
|
||||
public void Validate_OptionalNamesOmitted_Passes()
|
||||
{
|
||||
var result = validator.Validate(new CreateUserRequest(1, "Jane Doe", null, null));
|
||||
|
||||
[Fact]
|
||||
public void Validate_OptionalNamesOmitted_Passes()
|
||||
{
|
||||
var result = validator.Validate(new CreateUserRequest(1, "Jane Doe", null, null));
|
||||
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
}
|
||||
|
||||
+80
-81
@@ -5,108 +5,107 @@ using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Domain.Entities;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Albums
|
||||
namespace Musify.Application.Tests.Albums;
|
||||
|
||||
public sealed class AddTrackToAlbumCommandHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class AddTrackToAlbumCommandHandlerTests : HandlerTestBase
|
||||
private AddTrackToAlbumCommandHandler CreateHandler() => new(Database, NoOpLogger<AddTrackToAlbumCommandHandler>());
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_FirstTrack_AddsItAtNumberOne()
|
||||
{
|
||||
private AddTrackToAlbumCommandHandler CreateHandler() => new(Database, NoOpLogger<AddTrackToAlbumCommandHandler>());
|
||||
var owner = TestEntities.User();
|
||||
var album = TestEntities.Album(owner.Id);
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(owner, album, track);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_FirstTrack_AddsItAtNumberOne()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var album = TestEntities.Album(owner.Id);
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(owner, album, track);
|
||||
var result = await CreateHandler().Handle(new AddTrackToAlbumCommand(owner.Id, album.Id, track.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new AddTrackToAlbumCommand(owner.Id, album.Id, track.Id), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
var link = Assert.Single(await Database.AlbumHasTracks.ToListAsync(TestContext.Current.CancellationToken));
|
||||
Assert.Equal(1, link.TrackNumber);
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
var link = Assert.Single(await Database.AlbumHasTracks.ToListAsync(TestContext.Current.CancellationToken));
|
||||
Assert.Equal(1, link.TrackNumber);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_AlbumMissing_ReturnsNotFound()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(owner, track);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_AlbumMissing_ReturnsNotFound()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(owner, track);
|
||||
var result = await CreateHandler().Handle(new AddTrackToAlbumCommand(owner.Id, Guid.NewGuid(), track.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new AddTrackToAlbumCommand(owner.Id, Guid.NewGuid(), track.Id), TestContext.Current.CancellationToken);
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_NotOwner_ReturnsUnauthorized()
|
||||
{
|
||||
var owner = TestEntities.User(1, "owner");
|
||||
var stranger = TestEntities.User(2, "stranger");
|
||||
var album = TestEntities.Album(owner.Id);
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(owner, stranger, album, track);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NotOwner_ReturnsUnauthorized()
|
||||
{
|
||||
var owner = TestEntities.User(1, "owner");
|
||||
var stranger = TestEntities.User(2, "stranger");
|
||||
var album = TestEntities.Album(owner.Id);
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(owner, stranger, album, track);
|
||||
var result = await CreateHandler().Handle(new AddTrackToAlbumCommand(stranger.Id, album.Id, track.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new AddTrackToAlbumCommand(stranger.Id, album.Id, track.Id), TestContext.Current.CancellationToken);
|
||||
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
|
||||
}
|
||||
|
||||
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_TrackMissing_ReturnsNotFound()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var album = TestEntities.Album(owner.Id);
|
||||
await SeedAsync(owner, album);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_TrackMissing_ReturnsNotFound()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var album = TestEntities.Album(owner.Id);
|
||||
await SeedAsync(owner, album);
|
||||
var result = await CreateHandler().Handle(new AddTrackToAlbumCommand(owner.Id, album.Id, Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new AddTrackToAlbumCommand(owner.Id, album.Id, Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_TrackOwnedByAnotherUser_ReturnsUnauthorized()
|
||||
{
|
||||
var owner = TestEntities.User(1, "owner");
|
||||
var otherOwner = TestEntities.User(2, "other");
|
||||
var album = TestEntities.Album(owner.Id);
|
||||
var foreignTrack = TestEntities.Track(otherOwner);
|
||||
await SeedAsync(owner, otherOwner, album, foreignTrack);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_TrackOwnedByAnotherUser_ReturnsUnauthorized()
|
||||
{
|
||||
var owner = TestEntities.User(1, "owner");
|
||||
var otherOwner = TestEntities.User(2, "other");
|
||||
var album = TestEntities.Album(owner.Id);
|
||||
var foreignTrack = TestEntities.Track(otherOwner);
|
||||
await SeedAsync(owner, otherOwner, album, foreignTrack);
|
||||
var result = await CreateHandler().Handle(new AddTrackToAlbumCommand(owner.Id, album.Id, foreignTrack.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new AddTrackToAlbumCommand(owner.Id, album.Id, foreignTrack.Id), TestContext.Current.CancellationToken);
|
||||
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
|
||||
}
|
||||
|
||||
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_AlreadyInAlbum_ReturnsConflict()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var album = TestEntities.Album(owner.Id);
|
||||
var track = TestEntities.Track(owner);
|
||||
var link = new AlbumHasTrack { AlbumId = album.Id, TrackId = track.Id, TrackNumber = 1 };
|
||||
await SeedAsync(owner, album, track, link);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_AlreadyInAlbum_ReturnsConflict()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var album = TestEntities.Album(owner.Id);
|
||||
var track = TestEntities.Track(owner);
|
||||
var link = new AlbumHasTrack { AlbumId = album.Id, TrackId = track.Id, TrackNumber = 1 };
|
||||
await SeedAsync(owner, album, track, link);
|
||||
var result = await CreateHandler().Handle(new AddTrackToAlbumCommand(owner.Id, album.Id, track.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new AddTrackToAlbumCommand(owner.Id, album.Id, track.Id), TestContext.Current.CancellationToken);
|
||||
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
|
||||
}
|
||||
|
||||
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_AlbumAlreadyHasTracks_AppendsAtNextNumber()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var album = TestEntities.Album(owner.Id);
|
||||
var firstTrack = TestEntities.Track(owner, "First");
|
||||
var secondTrack = TestEntities.Track(owner, "Second");
|
||||
var existingLink = new AlbumHasTrack { AlbumId = album.Id, TrackId = firstTrack.Id, TrackNumber = 3 };
|
||||
await SeedAsync(owner, album, firstTrack, secondTrack, existingLink);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_AlbumAlreadyHasTracks_AppendsAtNextNumber()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var album = TestEntities.Album(owner.Id);
|
||||
var firstTrack = TestEntities.Track(owner, "First");
|
||||
var secondTrack = TestEntities.Track(owner, "Second");
|
||||
var existingLink = new AlbumHasTrack { AlbumId = album.Id, TrackId = firstTrack.Id, TrackNumber = 3 };
|
||||
await SeedAsync(owner, album, firstTrack, secondTrack, existingLink);
|
||||
var result = await CreateHandler().Handle(new AddTrackToAlbumCommand(owner.Id, album.Id, secondTrack.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new AddTrackToAlbumCommand(owner.Id, album.Id, secondTrack.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
var newLink = await Database.AlbumHasTracks.SingleAsync(l => l.TrackId == secondTrack.Id, TestContext.Current.CancellationToken);
|
||||
Assert.Equal(4, newLink.TrackNumber);
|
||||
}
|
||||
Assert.False(result.IsError);
|
||||
var newLink = await Database.AlbumHasTracks.SingleAsync(l => l.TrackId == secondTrack.Id, TestContext.Current.CancellationToken);
|
||||
Assert.Equal(4, newLink.TrackNumber);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,72 +6,71 @@ using Musify.Application.Tests.TestSupport;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Albums
|
||||
namespace Musify.Application.Tests.Albums;
|
||||
|
||||
public sealed class CreateAlbumCommandHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class CreateAlbumCommandHandlerTests : HandlerTestBase
|
||||
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
|
||||
private readonly IStorageService storageService = Substitute.For<IStorageService>();
|
||||
|
||||
private CreateAlbumCommandHandler CreateHandler() => new(
|
||||
eventBus,
|
||||
Database,
|
||||
new UploadIntentValidator(Database, storageService),
|
||||
NoOpLogger<CreateAlbumCommandHandler>(),
|
||||
TestConfigurations.Storage(),
|
||||
TestConfigurations.Album(),
|
||||
TestConfigurations.UploadIntent());
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ValidPictureIntent_CreatesAlbumAndPublishesResourcesEvent()
|
||||
{
|
||||
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
|
||||
private readonly IStorageService storageService = Substitute.For<IStorageService>();
|
||||
storageService
|
||||
.HeadObjectAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new ObjectMetaData("image/webp", 1024));
|
||||
|
||||
private CreateAlbumCommandHandler CreateHandler() => new(
|
||||
eventBus,
|
||||
Database,
|
||||
new UploadIntentValidator(Database, storageService),
|
||||
NoOpLogger<CreateAlbumCommandHandler>(),
|
||||
TestConfigurations.Storage(),
|
||||
TestConfigurations.Album(),
|
||||
TestConfigurations.UploadIntent());
|
||||
var owner = TestEntities.User();
|
||||
var intent = TestEntities.UploadIntent(owner.Id, Musify.Domain.ValueObjects.UploadIntentPurpose.AlbumPicture, objectName: "cover.webp");
|
||||
await SeedAsync(owner, intent);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ValidPictureIntent_CreatesAlbumAndPublishesResourcesEvent()
|
||||
{
|
||||
storageService
|
||||
.HeadObjectAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new ObjectMetaData("image/webp", 1024));
|
||||
var command = new CreateAlbumCommand(owner.Id, " My Album ", "A description", 2024, intent.Id);
|
||||
|
||||
var owner = TestEntities.User();
|
||||
var intent = TestEntities.UploadIntent(owner.Id, Musify.Domain.ValueObjects.UploadIntentPurpose.AlbumPicture, objectName: "cover.webp");
|
||||
await SeedAsync(owner, intent);
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
var command = new CreateAlbumCommand(owner.Id, " My Album ", "A description", 2024, intent.Id);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("My Album", result.Value.Title);
|
||||
Assert.Equal("A description", result.Value.Description);
|
||||
Assert.Equal(2024, result.Value.ReleaseYear);
|
||||
Assert.Equal(owner.Id, result.Value.OwnerUserId);
|
||||
Assert.Equal(0, result.Value.TrackCount);
|
||||
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
var stored = await Database.Albums.FindAsync([result.Value.Id], TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(stored);
|
||||
Assert.Equal("MY ALBUM", stored.NormalizedTitle);
|
||||
Assert.Equal("cover.webp", stored.Pictures?.OriginalName);
|
||||
await eventBus.Received(1).PublishAsync(Arg.Any<Musify.Application.Events.CreateAlbumResourcesEvent>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("My Album", result.Value.Title);
|
||||
Assert.Equal("A description", result.Value.Description);
|
||||
Assert.Equal(2024, result.Value.ReleaseYear);
|
||||
Assert.Equal(owner.Id, result.Value.OwnerUserId);
|
||||
Assert.Equal(0, result.Value.TrackCount);
|
||||
[Fact]
|
||||
public async Task Handle_UserDoesNotExist_ReturnsNotFound()
|
||||
{
|
||||
var command = new CreateAlbumCommand(404, "Orphan Album", null, null, Guid.NewGuid());
|
||||
|
||||
var stored = await Database.Albums.FindAsync([result.Value.Id], TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(stored);
|
||||
Assert.Equal("MY ALBUM", stored.NormalizedTitle);
|
||||
Assert.Equal("cover.webp", stored.Pictures?.OriginalName);
|
||||
await eventBus.Received(1).PublishAsync(Arg.Any<Musify.Application.Events.CreateAlbumResourcesEvent>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UserDoesNotExist_ReturnsNotFound()
|
||||
{
|
||||
var command = new CreateAlbumCommand(404, "Orphan Album", null, null, Guid.NewGuid());
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task Handle_PictureIntentNotFound_ReturnsError()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
await SeedAsync(owner);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
var command = new CreateAlbumCommand(owner.Id, "My Album", null, null, Guid.NewGuid());
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_PictureIntentNotFound_ReturnsError()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
await SeedAsync(owner);
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
var command = new CreateAlbumCommand(owner.Id, "My Album", null, null, Guid.NewGuid());
|
||||
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.True(result.IsError);
|
||||
}
|
||||
Assert.True(result.IsError);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,45 +3,44 @@ using Musify.Application.Albums;
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Albums
|
||||
namespace Musify.Application.Tests.Albums;
|
||||
|
||||
public sealed class DeleteAlbumCommandHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class DeleteAlbumCommandHandlerTests : HandlerTestBase
|
||||
private DeleteAlbumCommandHandler CreateHandler() => new(Database, NoOpLogger<DeleteAlbumCommandHandler>());
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_Owner_RemovesAlbum()
|
||||
{
|
||||
private DeleteAlbumCommandHandler CreateHandler() => new(Database, NoOpLogger<DeleteAlbumCommandHandler>());
|
||||
var owner = TestEntities.User();
|
||||
var album = TestEntities.Album(owner.Id);
|
||||
await SeedAsync(owner, album);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_Owner_RemovesAlbum()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var album = TestEntities.Album(owner.Id);
|
||||
await SeedAsync(owner, album);
|
||||
var result = await CreateHandler().Handle(new DeleteAlbumCommand(owner.Id, album.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new DeleteAlbumCommand(owner.Id, album.Id), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Null(await Database.Albums.FindAsync([album.Id], TestContext.Current.CancellationToken));
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Null(await Database.Albums.FindAsync([album.Id], TestContext.Current.CancellationToken));
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_AlbumMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new DeleteAlbumCommand(1, Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_AlbumMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new DeleteAlbumCommand(1, Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_NotOwner_ReturnsUnauthorizedAndKeepsAlbum()
|
||||
{
|
||||
var owner = TestEntities.User(1, "owner");
|
||||
var stranger = TestEntities.User(2, "stranger");
|
||||
var album = TestEntities.Album(owner.Id);
|
||||
await SeedAsync(owner, stranger, album);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NotOwner_ReturnsUnauthorizedAndKeepsAlbum()
|
||||
{
|
||||
var owner = TestEntities.User(1, "owner");
|
||||
var stranger = TestEntities.User(2, "stranger");
|
||||
var album = TestEntities.Album(owner.Id);
|
||||
await SeedAsync(owner, stranger, album);
|
||||
var result = await CreateHandler().Handle(new DeleteAlbumCommand(stranger.Id, album.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new DeleteAlbumCommand(stranger.Id, album.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
|
||||
Assert.NotNull(await Database.Albums.FindAsync([album.Id], TestContext.Current.CancellationToken));
|
||||
}
|
||||
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
|
||||
Assert.NotNull(await Database.Albums.FindAsync([album.Id], TestContext.Current.CancellationToken));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,34 +4,33 @@ using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Domain.Entities;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Albums
|
||||
namespace Musify.Application.Tests.Albums;
|
||||
|
||||
public sealed class GetAlbumByIdQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class GetAlbumByIdQueryHandlerTests : HandlerTestBase
|
||||
private GetAlbumByIdQueryHandler CreateHandler() => new(Database);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ExistingAlbum_ReturnsItWithTrackCountAndCovers()
|
||||
{
|
||||
private GetAlbumByIdQueryHandler CreateHandler() => new(Database);
|
||||
var owner = TestEntities.User();
|
||||
var album = TestEntities.Album(owner.Id);
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(owner, album, track, new AlbumHasTrack { AlbumId = album.Id, TrackId = track.Id, TrackNumber = 1 });
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ExistingAlbum_ReturnsItWithTrackCountAndCovers()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var album = TestEntities.Album(owner.Id);
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(owner, album, track, new AlbumHasTrack { AlbumId = album.Id, TrackId = track.Id, TrackNumber = 1 });
|
||||
var result = await CreateHandler().Handle(new GetAlbumByIdQuery(album.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetAlbumByIdQuery(album.Id), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(album.Id, result.Value.Id);
|
||||
Assert.Equal(1, result.Value.TrackCount);
|
||||
Assert.Equal([track.Id], result.Value.CoverTrackIds);
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(album.Id, result.Value.Id);
|
||||
Assert.Equal(1, result.Value.TrackCount);
|
||||
Assert.Equal([track.Id], result.Value.CoverTrackIds);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_AlbumMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new GetAlbumByIdQuery(Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_AlbumMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new GetAlbumByIdQuery(Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,36 +3,35 @@ using Musify.Application.Albums;
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Albums
|
||||
namespace Musify.Application.Tests.Albums;
|
||||
|
||||
public sealed class GetAlbumCoverQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class GetAlbumCoverQueryHandlerTests : HandlerTestBase
|
||||
private GetAlbumCoverQueryHandler CreateHandler() =>
|
||||
new(Database, TestConfigurations.Storage("covers-bucket"), TestConfigurations.Album());
|
||||
|
||||
[Theory]
|
||||
[InlineData("small", "small.webp")]
|
||||
[InlineData("large", "large.webp")]
|
||||
[InlineData("medium", "medium.webp")]
|
||||
public async Task Handle_ExistingAlbum_ReturnsRequestedSize(string size, string expectedFileName)
|
||||
{
|
||||
private GetAlbumCoverQueryHandler CreateHandler() =>
|
||||
new(Database, TestConfigurations.Storage("covers-bucket"), TestConfigurations.Album());
|
||||
var owner = TestEntities.User();
|
||||
var album = TestEntities.Album(owner.Id);
|
||||
await SeedAsync(owner, album);
|
||||
|
||||
[Theory]
|
||||
[InlineData("small", "small.webp")]
|
||||
[InlineData("large", "large.webp")]
|
||||
[InlineData("medium", "medium.webp")]
|
||||
public async Task Handle_ExistingAlbum_ReturnsRequestedSize(string size, string expectedFileName)
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var album = TestEntities.Album(owner.Id);
|
||||
await SeedAsync(owner, album);
|
||||
var result = await CreateHandler().Handle(new GetAlbumCoverQuery(album.Id, size), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetAlbumCoverQuery(album.Id, size), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("covers-bucket", result.Value.Bucket);
|
||||
Assert.EndsWith(expectedFileName, result.Value.Key);
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("covers-bucket", result.Value.Bucket);
|
||||
Assert.EndsWith(expectedFileName, result.Value.Key);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_AlbumMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new GetAlbumCoverQuery(Guid.NewGuid(), "small"), TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_AlbumMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new GetAlbumCoverQuery(Guid.NewGuid(), "small"), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,36 +4,35 @@ using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Domain.Entities;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Albums
|
||||
namespace Musify.Application.Tests.Albums;
|
||||
|
||||
public sealed class GetAlbumTracksQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class GetAlbumTracksQueryHandlerTests : HandlerTestBase
|
||||
private GetAlbumTracksQueryHandler CreateHandler() => new(Database);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_AlbumWithTracks_ReturnsThemOrderedByTrackNumber()
|
||||
{
|
||||
private GetAlbumTracksQueryHandler CreateHandler() => new(Database);
|
||||
var owner = TestEntities.User();
|
||||
var album = TestEntities.Album(owner.Id);
|
||||
var second = TestEntities.Track(owner, "Second");
|
||||
var first = TestEntities.Track(owner, "First");
|
||||
await SeedAsync(
|
||||
owner, album, second, first,
|
||||
new AlbumHasTrack { AlbumId = album.Id, TrackId = second.Id, TrackNumber = 2 },
|
||||
new AlbumHasTrack { AlbumId = album.Id, TrackId = first.Id, TrackNumber = 1 });
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_AlbumWithTracks_ReturnsThemOrderedByTrackNumber()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var album = TestEntities.Album(owner.Id);
|
||||
var second = TestEntities.Track(owner, "Second");
|
||||
var first = TestEntities.Track(owner, "First");
|
||||
await SeedAsync(
|
||||
owner, album, second, first,
|
||||
new AlbumHasTrack { AlbumId = album.Id, TrackId = second.Id, TrackNumber = 2 },
|
||||
new AlbumHasTrack { AlbumId = album.Id, TrackId = first.Id, TrackNumber = 1 });
|
||||
var result = await CreateHandler().Handle(new GetAlbumTracksQuery(album.Id, PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetAlbumTracksQuery(album.Id, PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(["First", "Second"], result.Value.Items.Select(track => track.Title));
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(["First", "Second"], result.Value.Items.Select(track => track.Title));
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_AlbumMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new GetAlbumTracksQuery(Guid.NewGuid(), PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_AlbumMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new GetAlbumTracksQuery(Guid.NewGuid(), PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
}
|
||||
|
||||
+20
-21
@@ -3,32 +3,31 @@ using Musify.Application.Albums;
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Albums
|
||||
namespace Musify.Application.Tests.Albums;
|
||||
|
||||
public sealed class GetAlbumsByUserIdQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class GetAlbumsByUserIdQueryHandlerTests : HandlerTestBase
|
||||
private GetAlbumsByUserIdQueryHandler CreateHandler() => new(Database);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UserWithAlbums_ReturnsOnlyTheirAlbums()
|
||||
{
|
||||
private GetAlbumsByUserIdQueryHandler CreateHandler() => new(Database);
|
||||
var owner = TestEntities.User(1, "owner");
|
||||
var other = TestEntities.User(2, "other");
|
||||
await SeedAsync(owner, other, TestEntities.Album(owner.Id, "Mine"), TestEntities.Album(other.Id, "Theirs"));
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UserWithAlbums_ReturnsOnlyTheirAlbums()
|
||||
{
|
||||
var owner = TestEntities.User(1, "owner");
|
||||
var other = TestEntities.User(2, "other");
|
||||
await SeedAsync(owner, other, TestEntities.Album(owner.Id, "Mine"), TestEntities.Album(other.Id, "Theirs"));
|
||||
var result = await CreateHandler().Handle(new GetAlbumsByUserIdQuery(owner.Id, PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetAlbumsByUserIdQuery(owner.Id, PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
var album = Assert.Single(result.Value.Items);
|
||||
Assert.Equal("Mine", album.Title);
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
var album = Assert.Single(result.Value.Items);
|
||||
Assert.Equal("Mine", album.Title);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_UserMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new GetAlbumsByUserIdQuery(404, PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UserMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new GetAlbumsByUserIdQuery(404, PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,49 +2,48 @@ using Musify.Application.Albums;
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Albums
|
||||
namespace Musify.Application.Tests.Albums;
|
||||
|
||||
public sealed class GetAlbumsQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class GetAlbumsQueryHandlerTests : HandlerTestBase
|
||||
private GetAlbumsQueryHandler CreateHandler() => new(Database);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NoTitleFilter_ReturnsAllAlbumsPaged()
|
||||
{
|
||||
private GetAlbumsQueryHandler CreateHandler() => new(Database);
|
||||
var owner = TestEntities.User();
|
||||
await SeedAsync(owner, TestEntities.Album(owner.Id, "Alpha"), TestEntities.Album(owner.Id, "Beta"));
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NoTitleFilter_ReturnsAllAlbumsPaged()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
await SeedAsync(owner, TestEntities.Album(owner.Id, "Alpha"), TestEntities.Album(owner.Id, "Beta"));
|
||||
var result = await CreateHandler().Handle(new GetAlbumsQuery(Title: null, PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetAlbumsQuery(Title: null, PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(2, result.Value.TotalItemCount);
|
||||
Assert.All(result.Value.Items, item => Assert.NotNull(item.Album));
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(2, result.Value.TotalItemCount);
|
||||
Assert.All(result.Value.Items, item => Assert.NotNull(item.Album));
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_TitleFilter_ReturnsMatchingAlbumsOnly()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
await SeedAsync(owner, TestEntities.Album(owner.Id, "Rock Classics"), TestEntities.Album(owner.Id, "Jazz Favorites"));
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_TitleFilter_ReturnsMatchingAlbumsOnly()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
await SeedAsync(owner, TestEntities.Album(owner.Id, "Rock Classics"), TestEntities.Album(owner.Id, "Jazz Favorites"));
|
||||
var result = await CreateHandler().Handle(new GetAlbumsQuery(Title: "rock", PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetAlbumsQuery(Title: "rock", PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
var item = Assert.Single(result.Value.Items);
|
||||
Assert.Equal("Rock Classics", item.Album.Title);
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
var item = Assert.Single(result.Value.Items);
|
||||
Assert.Equal("Rock Classics", item.Album.Title);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_PageSizeSmallerThanTotal_ReportsHasNextPage()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
await SeedAsync(owner, TestEntities.Album(owner.Id, "One"), TestEntities.Album(owner.Id, "Two"), TestEntities.Album(owner.Id, "Three"));
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_PageSizeSmallerThanTotal_ReportsHasNextPage()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
await SeedAsync(owner, TestEntities.Album(owner.Id, "One"), TestEntities.Album(owner.Id, "Two"), TestEntities.Album(owner.Id, "Three"));
|
||||
var result = await CreateHandler().Handle(new GetAlbumsQuery(Title: null, PageNumber: 1, PageSize: 2), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetAlbumsQuery(Title: null, PageNumber: 1, PageSize: 2), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(2, result.Value.Items.Count);
|
||||
Assert.True(result.Value.HasNextPage);
|
||||
}
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(2, result.Value.Items.Count);
|
||||
Assert.True(result.Value.HasNextPage);
|
||||
}
|
||||
}
|
||||
|
||||
+60
-61
@@ -3,79 +3,78 @@ using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Domain.Entities;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Albums
|
||||
namespace Musify.Application.Tests.Albums;
|
||||
|
||||
public sealed class GetRecentlyListenedAlbumsQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class GetRecentlyListenedAlbumsQueryHandlerTests : HandlerTestBase
|
||||
private GetRecentlyListenedAlbumsQueryHandler CreateHandler() => new(Database);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ListenedTracks_ReturnsTheirAlbumsMostRecentFirst()
|
||||
{
|
||||
private GetRecentlyListenedAlbumsQueryHandler CreateHandler() => new(Database);
|
||||
var owner = TestEntities.User();
|
||||
var olderAlbum = TestEntities.Album(owner.Id, "Older");
|
||||
var newerAlbum = TestEntities.Album(owner.Id, "Newer");
|
||||
var olderTrack = TestEntities.Track(owner, "In older album");
|
||||
var newerTrack = TestEntities.Track(owner, "In newer album");
|
||||
await SeedAsync(
|
||||
owner, olderAlbum, newerAlbum, olderTrack, newerTrack,
|
||||
new AlbumHasTrack { AlbumId = olderAlbum.Id, TrackId = olderTrack.Id, TrackNumber = 1 },
|
||||
new AlbumHasTrack { AlbumId = newerAlbum.Id, TrackId = newerTrack.Id, TrackNumber = 1 },
|
||||
TestEntities.ListeningHistory(owner.Id, olderTrack.Id, DateTime.UtcNow.AddDays(-2)),
|
||||
TestEntities.ListeningHistory(owner.Id, newerTrack.Id, DateTime.UtcNow.AddDays(-1)));
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ListenedTracks_ReturnsTheirAlbumsMostRecentFirst()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var olderAlbum = TestEntities.Album(owner.Id, "Older");
|
||||
var newerAlbum = TestEntities.Album(owner.Id, "Newer");
|
||||
var olderTrack = TestEntities.Track(owner, "In older album");
|
||||
var newerTrack = TestEntities.Track(owner, "In newer album");
|
||||
await SeedAsync(
|
||||
owner, olderAlbum, newerAlbum, olderTrack, newerTrack,
|
||||
new AlbumHasTrack { AlbumId = olderAlbum.Id, TrackId = olderTrack.Id, TrackNumber = 1 },
|
||||
new AlbumHasTrack { AlbumId = newerAlbum.Id, TrackId = newerTrack.Id, TrackNumber = 1 },
|
||||
TestEntities.ListeningHistory(owner.Id, olderTrack.Id, DateTime.UtcNow.AddDays(-2)),
|
||||
TestEntities.ListeningHistory(owner.Id, newerTrack.Id, DateTime.UtcNow.AddDays(-1)));
|
||||
var result = await CreateHandler().Handle(new GetRecentlyListenedAlbumsQuery(owner.Id, Limit: 10), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetRecentlyListenedAlbumsQuery(owner.Id, Limit: 10), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(["Newer", "Older"], result.Value.Select(album => album.Title));
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(["Newer", "Older"], result.Value.Select(album => album.Title));
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_NoListeningHistory_ReturnsEmptyList()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
await SeedAsync(owner);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NoListeningHistory_ReturnsEmptyList()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
await SeedAsync(owner);
|
||||
var result = await CreateHandler().Handle(new GetRecentlyListenedAlbumsQuery(owner.Id, Limit: 10), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetRecentlyListenedAlbumsQuery(owner.Id, Limit: 10), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Empty(result.Value);
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Empty(result.Value);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_LimitLowerThanCandidates_ReturnsOnlyThatManyAlbums()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var albums = Enumerable.Range(1, 3).Select(i => TestEntities.Album(owner.Id, $"Album {i}")).ToList();
|
||||
var tracks = albums.Select((_, i) => TestEntities.Track(owner, $"Track {i}")).ToList();
|
||||
var links = albums.Zip(tracks, (album, track) => new AlbumHasTrack { AlbumId = album.Id, TrackId = track.Id, TrackNumber = 1 }).ToList();
|
||||
var histories = tracks.Select((track, i) => TestEntities.ListeningHistory(owner.Id, track.Id, DateTime.UtcNow.AddMinutes(-i))).ToList();
|
||||
await SeedAsync([owner, .. albums, .. tracks, .. links, .. histories]);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_LimitLowerThanCandidates_ReturnsOnlyThatManyAlbums()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var albums = Enumerable.Range(1, 3).Select(i => TestEntities.Album(owner.Id, $"Album {i}")).ToList();
|
||||
var tracks = albums.Select((_, i) => TestEntities.Track(owner, $"Track {i}")).ToList();
|
||||
var links = albums.Zip(tracks, (album, track) => new AlbumHasTrack { AlbumId = album.Id, TrackId = track.Id, TrackNumber = 1 }).ToList();
|
||||
var histories = tracks.Select((track, i) => TestEntities.ListeningHistory(owner.Id, track.Id, DateTime.UtcNow.AddMinutes(-i))).ToList();
|
||||
await SeedAsync([owner, .. albums, .. tracks, .. links, .. histories]);
|
||||
var result = await CreateHandler().Handle(new GetRecentlyListenedAlbumsQuery(owner.Id, Limit: 2), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetRecentlyListenedAlbumsQuery(owner.Id, Limit: 2), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(2, result.Value.Count);
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(2, result.Value.Count);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_UncountedListens_AreIgnored()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var countedAlbum = TestEntities.Album(owner.Id, "Counted album");
|
||||
var skippedAlbum = TestEntities.Album(owner.Id, "Skipped album");
|
||||
var countedTrack = TestEntities.Track(owner, "In counted");
|
||||
var skippedTrack = TestEntities.Track(owner, "In skipped");
|
||||
await SeedAsync(
|
||||
owner, countedAlbum, skippedAlbum, countedTrack, skippedTrack,
|
||||
new AlbumHasTrack { AlbumId = countedAlbum.Id, TrackId = countedTrack.Id, TrackNumber = 1 },
|
||||
new AlbumHasTrack { AlbumId = skippedAlbum.Id, TrackId = skippedTrack.Id, TrackNumber = 1 },
|
||||
TestEntities.ListeningHistory(owner.Id, countedTrack.Id, DateTime.UtcNow.AddDays(-1), playedSeconds: 120),
|
||||
TestEntities.ListeningHistory(owner.Id, skippedTrack.Id, DateTime.UtcNow, playedSeconds: 1, isCounted: false));
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UncountedListens_AreIgnored()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var countedAlbum = TestEntities.Album(owner.Id, "Counted album");
|
||||
var skippedAlbum = TestEntities.Album(owner.Id, "Skipped album");
|
||||
var countedTrack = TestEntities.Track(owner, "In counted");
|
||||
var skippedTrack = TestEntities.Track(owner, "In skipped");
|
||||
await SeedAsync(
|
||||
owner, countedAlbum, skippedAlbum, countedTrack, skippedTrack,
|
||||
new AlbumHasTrack { AlbumId = countedAlbum.Id, TrackId = countedTrack.Id, TrackNumber = 1 },
|
||||
new AlbumHasTrack { AlbumId = skippedAlbum.Id, TrackId = skippedTrack.Id, TrackNumber = 1 },
|
||||
TestEntities.ListeningHistory(owner.Id, countedTrack.Id, DateTime.UtcNow.AddDays(-1), playedSeconds: 120),
|
||||
TestEntities.ListeningHistory(owner.Id, skippedTrack.Id, DateTime.UtcNow, playedSeconds: 1, isCounted: false));
|
||||
var result = await CreateHandler().Handle(new GetRecentlyListenedAlbumsQuery(owner.Id, Limit: 10), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetRecentlyListenedAlbumsQuery(owner.Id, Limit: 10), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(["Counted album"], result.Value.Select(album => album.Title));
|
||||
}
|
||||
Assert.Equal(["Counted album"], result.Value.Select(album => album.Title));
|
||||
}
|
||||
}
|
||||
|
||||
+51
-52
@@ -5,69 +5,68 @@ using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Domain.Entities;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Albums
|
||||
namespace Musify.Application.Tests.Albums;
|
||||
|
||||
public sealed class RemoveTrackFromAlbumCommandHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class RemoveTrackFromAlbumCommandHandlerTests : HandlerTestBase
|
||||
private RemoveTrackFromAlbumCommandHandler CreateHandler() =>
|
||||
new(Database, NoOpLogger<RemoveTrackFromAlbumCommandHandler>());
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_TrackInAlbum_RemovesItAndRenumbersTheRest()
|
||||
{
|
||||
private RemoveTrackFromAlbumCommandHandler CreateHandler() =>
|
||||
new(Database, NoOpLogger<RemoveTrackFromAlbumCommandHandler>());
|
||||
var owner = TestEntities.User();
|
||||
var album = TestEntities.Album(owner.Id);
|
||||
var first = TestEntities.Track(owner, "First");
|
||||
var second = TestEntities.Track(owner, "Second");
|
||||
var third = TestEntities.Track(owner, "Third");
|
||||
await SeedAsync(
|
||||
owner, album, first, second, third,
|
||||
new AlbumHasTrack { AlbumId = album.Id, TrackId = first.Id, TrackNumber = 1 },
|
||||
new AlbumHasTrack { AlbumId = album.Id, TrackId = second.Id, TrackNumber = 2 },
|
||||
new AlbumHasTrack { AlbumId = album.Id, TrackId = third.Id, TrackNumber = 3 });
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_TrackInAlbum_RemovesItAndRenumbersTheRest()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var album = TestEntities.Album(owner.Id);
|
||||
var first = TestEntities.Track(owner, "First");
|
||||
var second = TestEntities.Track(owner, "Second");
|
||||
var third = TestEntities.Track(owner, "Third");
|
||||
await SeedAsync(
|
||||
owner, album, first, second, third,
|
||||
new AlbumHasTrack { AlbumId = album.Id, TrackId = first.Id, TrackNumber = 1 },
|
||||
new AlbumHasTrack { AlbumId = album.Id, TrackId = second.Id, TrackNumber = 2 },
|
||||
new AlbumHasTrack { AlbumId = album.Id, TrackId = third.Id, TrackNumber = 3 });
|
||||
var result = await CreateHandler().Handle(new RemoveTrackFromAlbumCommand(owner.Id, album.Id, second.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new RemoveTrackFromAlbumCommand(owner.Id, album.Id, second.Id), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
var remaining = await Database.AlbumHasTracks
|
||||
.Where(link => link.AlbumId == album.Id)
|
||||
.OrderBy(link => link.TrackNumber)
|
||||
.ToListAsync(TestContext.Current.CancellationToken);
|
||||
Assert.Equal([first.Id, third.Id], remaining.Select(link => link.TrackId));
|
||||
Assert.Equal([1, 2], remaining.Select(link => link.TrackNumber));
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
var remaining = await Database.AlbumHasTracks
|
||||
.Where(link => link.AlbumId == album.Id)
|
||||
.OrderBy(link => link.TrackNumber)
|
||||
.ToListAsync(TestContext.Current.CancellationToken);
|
||||
Assert.Equal([first.Id, third.Id], remaining.Select(link => link.TrackId));
|
||||
Assert.Equal([1, 2], remaining.Select(link => link.TrackNumber));
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_AlbumMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new RemoveTrackFromAlbumCommand(1, Guid.NewGuid(), Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_AlbumMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new RemoveTrackFromAlbumCommand(1, Guid.NewGuid(), Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_NotOwner_ReturnsUnauthorized()
|
||||
{
|
||||
var owner = TestEntities.User(1, "owner");
|
||||
var stranger = TestEntities.User(2, "stranger");
|
||||
var album = TestEntities.Album(owner.Id);
|
||||
await SeedAsync(owner, stranger, album);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NotOwner_ReturnsUnauthorized()
|
||||
{
|
||||
var owner = TestEntities.User(1, "owner");
|
||||
var stranger = TestEntities.User(2, "stranger");
|
||||
var album = TestEntities.Album(owner.Id);
|
||||
await SeedAsync(owner, stranger, album);
|
||||
var result = await CreateHandler().Handle(new RemoveTrackFromAlbumCommand(stranger.Id, album.Id, Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new RemoveTrackFromAlbumCommand(stranger.Id, album.Id, Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
|
||||
}
|
||||
|
||||
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_TrackNotInAlbum_ReturnsNotFound()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var album = TestEntities.Album(owner.Id);
|
||||
await SeedAsync(owner, album);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_TrackNotInAlbum_ReturnsNotFound()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var album = TestEntities.Album(owner.Id);
|
||||
await SeedAsync(owner, album);
|
||||
var result = await CreateHandler().Handle(new RemoveTrackFromAlbumCommand(owner.Id, album.Id, Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new RemoveTrackFromAlbumCommand(owner.Id, album.Id, Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
}
|
||||
|
||||
+47
-48
@@ -8,68 +8,67 @@ using Musify.Application.Tests.TestSupport;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Albums
|
||||
namespace Musify.Application.Tests.Albums;
|
||||
|
||||
public sealed class RequestAlbumPictureUploadCommandHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class RequestAlbumPictureUploadCommandHandlerTests : HandlerTestBase
|
||||
private readonly IStorageService storageService = Substitute.For<IStorageService>();
|
||||
|
||||
private RequestAlbumPictureUploadCommandHandler CreateHandler(UploadIntentConfiguration? uploadIntentConfig = null)
|
||||
{
|
||||
private readonly IStorageService storageService = Substitute.For<IStorageService>();
|
||||
storageService
|
||||
.GetUploadUrlAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<TimeSpan>(), Arg.Any<CancellationToken>())
|
||||
.Returns("https://storage.musify.test/presigned-upload");
|
||||
|
||||
private RequestAlbumPictureUploadCommandHandler CreateHandler(UploadIntentConfiguration? uploadIntentConfig = null)
|
||||
{
|
||||
storageService
|
||||
.GetUploadUrlAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<TimeSpan>(), Arg.Any<CancellationToken>())
|
||||
.Returns("https://storage.musify.test/presigned-upload");
|
||||
return new RequestAlbumPictureUploadCommandHandler(
|
||||
Database,
|
||||
new UploadIntentValidator(Database, storageService),
|
||||
storageService,
|
||||
NoOpLogger<RequestAlbumPictureUploadCommandHandler>(),
|
||||
TestConfigurations.Storage(),
|
||||
TestConfigurations.Album(),
|
||||
uploadIntentConfig ?? TestConfigurations.UploadIntent());
|
||||
}
|
||||
|
||||
return new RequestAlbumPictureUploadCommandHandler(
|
||||
Database,
|
||||
new UploadIntentValidator(Database, storageService),
|
||||
storageService,
|
||||
NoOpLogger<RequestAlbumPictureUploadCommandHandler>(),
|
||||
TestConfigurations.Storage(),
|
||||
TestConfigurations.Album(),
|
||||
uploadIntentConfig ?? TestConfigurations.UploadIntent());
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_UserExists_IssuesUploadUrlAndCreatesIntent()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
await SeedAsync(user);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UserExists_IssuesUploadUrlAndCreatesIntent()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
await SeedAsync(user);
|
||||
var command = new RequestAlbumPictureUploadCommand(user.Id, "webp", "image/webp");
|
||||
|
||||
var command = new RequestAlbumPictureUploadCommand(user.Id, "webp", "image/webp");
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("https://storage.musify.test/presigned-upload", result.Value.UploadUrl);
|
||||
Assert.Single(await Database.UploadIntents.ToListAsync(TestContext.Current.CancellationToken));
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("https://storage.musify.test/presigned-upload", result.Value.UploadUrl);
|
||||
Assert.Single(await Database.UploadIntents.ToListAsync(TestContext.Current.CancellationToken));
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_UserMissing_ReturnsNotFound()
|
||||
{
|
||||
var command = new RequestAlbumPictureUploadCommand(404, "webp", "image/webp");
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UserMissing_ReturnsNotFound()
|
||||
{
|
||||
var command = new RequestAlbumPictureUploadCommand(404, "webp", "image/webp");
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_UserOverByteQuota_ReturnsValidationErrorAndCreatesNoIntent()
|
||||
{
|
||||
var config = TestConfigurations.UploadIntent();
|
||||
config.MaxActiveUploadBytesPerUser = 100;
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UserOverByteQuota_ReturnsValidationErrorAndCreatesNoIntent()
|
||||
{
|
||||
var config = TestConfigurations.UploadIntent();
|
||||
config.MaxActiveUploadBytesPerUser = 100;
|
||||
var user = TestEntities.User();
|
||||
await SeedAsync(user);
|
||||
|
||||
var user = TestEntities.User();
|
||||
await SeedAsync(user);
|
||||
var command = new RequestAlbumPictureUploadCommand(user.Id, "webp", "image/webp", ExpectedSizeBytes: 1_000_000);
|
||||
|
||||
var command = new RequestAlbumPictureUploadCommand(user.Id, "webp", "image/webp", ExpectedSizeBytes: 1_000_000);
|
||||
var result = await CreateHandler(config).Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler(config).Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
|
||||
Assert.Empty(await Database.UploadIntents.ToListAsync(TestContext.Current.CancellationToken));
|
||||
}
|
||||
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
|
||||
Assert.Empty(await Database.UploadIntents.ToListAsync(TestContext.Current.CancellationToken));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,89 +6,88 @@ using Musify.Application.Tests.TestSupport;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Albums
|
||||
namespace Musify.Application.Tests.Albums;
|
||||
|
||||
public sealed class UpdateAlbumCommandHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class UpdateAlbumCommandHandlerTests : HandlerTestBase
|
||||
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
|
||||
private readonly IStorageService storageService = Substitute.For<IStorageService>();
|
||||
|
||||
private UpdateAlbumCommandHandler CreateHandler() => new(
|
||||
eventBus,
|
||||
Database,
|
||||
new UploadIntentValidator(Database, storageService),
|
||||
NoOpLogger<UpdateAlbumCommandHandler>(),
|
||||
TestConfigurations.Storage(),
|
||||
TestConfigurations.Album(),
|
||||
TestConfigurations.UploadIntent());
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_Owner_UpdatesTitleDescriptionAndReleaseYear()
|
||||
{
|
||||
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
|
||||
private readonly IStorageService storageService = Substitute.For<IStorageService>();
|
||||
var owner = TestEntities.User();
|
||||
var album = TestEntities.Album(owner.Id, title: "Old Title", description: "Old", releaseYear: 2020);
|
||||
await SeedAsync(owner, album);
|
||||
|
||||
private UpdateAlbumCommandHandler CreateHandler() => new(
|
||||
eventBus,
|
||||
Database,
|
||||
new UploadIntentValidator(Database, storageService),
|
||||
NoOpLogger<UpdateAlbumCommandHandler>(),
|
||||
TestConfigurations.Storage(),
|
||||
TestConfigurations.Album(),
|
||||
TestConfigurations.UploadIntent());
|
||||
var command = new UpdateAlbumCommand(owner.Id, album.Id, " New Title ", "New description", 2025, null);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_Owner_UpdatesTitleDescriptionAndReleaseYear()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var album = TestEntities.Album(owner.Id, title: "Old Title", description: "Old", releaseYear: 2020);
|
||||
await SeedAsync(owner, album);
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
var command = new UpdateAlbumCommand(owner.Id, album.Id, " New Title ", "New description", 2025, null);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("New Title", result.Value.Title);
|
||||
Assert.Equal("New description", result.Value.Description);
|
||||
Assert.Equal(2025, result.Value.ReleaseYear);
|
||||
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
var stored = await Database.Albums.FindAsync([album.Id], TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(stored);
|
||||
Assert.Equal("NEW TITLE", stored.NormalizedTitle);
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("New Title", result.Value.Title);
|
||||
Assert.Equal("New description", result.Value.Description);
|
||||
Assert.Equal(2025, result.Value.ReleaseYear);
|
||||
[Fact]
|
||||
public async Task Handle_ValidPictureIntent_UpdatesPictureAndPublishesSourceEvent()
|
||||
{
|
||||
storageService
|
||||
.HeadObjectAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new ObjectMetaData("image/webp", 1024));
|
||||
|
||||
var stored = await Database.Albums.FindAsync([album.Id], TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(stored);
|
||||
Assert.Equal("NEW TITLE", stored.NormalizedTitle);
|
||||
}
|
||||
var owner = TestEntities.User();
|
||||
var album = TestEntities.Album(owner.Id, title: "Old Title");
|
||||
var intent = TestEntities.UploadIntent(owner.Id, Musify.Domain.ValueObjects.UploadIntentPurpose.AlbumPicture, objectName: "new-cover.webp");
|
||||
await SeedAsync(owner, album, intent);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ValidPictureIntent_UpdatesPictureAndPublishesSourceEvent()
|
||||
{
|
||||
storageService
|
||||
.HeadObjectAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new ObjectMetaData("image/webp", 1024));
|
||||
var command = new UpdateAlbumCommand(owner.Id, album.Id, "Old Title", null, null, intent.Id);
|
||||
|
||||
var owner = TestEntities.User();
|
||||
var album = TestEntities.Album(owner.Id, title: "Old Title");
|
||||
var intent = TestEntities.UploadIntent(owner.Id, Musify.Domain.ValueObjects.UploadIntentPurpose.AlbumPicture, objectName: "new-cover.webp");
|
||||
await SeedAsync(owner, album, intent);
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
var command = new UpdateAlbumCommand(owner.Id, album.Id, "Old Title", null, null, intent.Id);
|
||||
Assert.False(result.IsError);
|
||||
var stored = await Database.Albums.FindAsync([album.Id], TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(stored);
|
||||
Assert.Equal("new-cover.webp", stored.Pictures?.OriginalName);
|
||||
await eventBus.Received(1).PublishAsync(Arg.Any<Musify.Application.Events.UpdateAlbumPictureSourceEvent>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task Handle_AlbumMissing_ReturnsNotFound()
|
||||
{
|
||||
var command = new UpdateAlbumCommand(1, Guid.NewGuid(), "Title", null, null, null);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
var stored = await Database.Albums.FindAsync([album.Id], TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(stored);
|
||||
Assert.Equal("new-cover.webp", stored.Pictures?.OriginalName);
|
||||
await eventBus.Received(1).PublishAsync(Arg.Any<Musify.Application.Events.UpdateAlbumPictureSourceEvent>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_AlbumMissing_ReturnsNotFound()
|
||||
{
|
||||
var command = new UpdateAlbumCommand(1, Guid.NewGuid(), "Title", null, null, null);
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task Handle_NotOwner_ReturnsUnauthorized()
|
||||
{
|
||||
var owner = TestEntities.User(1, "owner");
|
||||
var stranger = TestEntities.User(2, "stranger");
|
||||
var album = TestEntities.Album(owner.Id);
|
||||
await SeedAsync(owner, stranger, album);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
var command = new UpdateAlbumCommand(stranger.Id, album.Id, "Hijacked", null, null, null);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NotOwner_ReturnsUnauthorized()
|
||||
{
|
||||
var owner = TestEntities.User(1, "owner");
|
||||
var stranger = TestEntities.User(2, "stranger");
|
||||
var album = TestEntities.Album(owner.Id);
|
||||
await SeedAsync(owner, stranger, album);
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
var command = new UpdateAlbumCommand(stranger.Id, album.Id, "Hijacked", null, null, null);
|
||||
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
|
||||
}
|
||||
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
|
||||
}
|
||||
}
|
||||
|
||||
+17
-18
@@ -2,30 +2,29 @@ using Musify.Application.Configuration;
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Configuration
|
||||
namespace Musify.Application.Tests.Configuration;
|
||||
|
||||
public sealed class GetPlaybackPublicConfigTests
|
||||
{
|
||||
public sealed class GetPlaybackPublicConfigTests
|
||||
[Fact]
|
||||
public async Task Handle_ReflectsPlaybackConfiguration()
|
||||
{
|
||||
[Fact]
|
||||
public async Task Handle_ReflectsPlaybackConfiguration()
|
||||
{
|
||||
var playback = TestConfigurations.Playback(allowAnonymousListening: true, anonymousFragmentSeconds: 30);
|
||||
var handler = new GetPlaybackPublicConfigQueryHandler(playback);
|
||||
var playback = TestConfigurations.Playback(allowAnonymousListening: true, anonymousFragmentSeconds: 30);
|
||||
var handler = new GetPlaybackPublicConfigQueryHandler(playback);
|
||||
|
||||
var response = await handler.Handle(new GetPlaybackPublicConfigQuery(), TestContext.Current.CancellationToken);
|
||||
var response = await handler.Handle(new GetPlaybackPublicConfigQuery(), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.True(response.AllowAnonymousListening);
|
||||
Assert.Equal(30, response.AnonymousFragmentSeconds);
|
||||
}
|
||||
Assert.True(response.AllowAnonymousListening);
|
||||
Assert.Equal(30, response.AnonymousFragmentSeconds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_DefaultsToDisabled()
|
||||
{
|
||||
var handler = new GetPlaybackPublicConfigQueryHandler(TestConfigurations.Playback());
|
||||
[Fact]
|
||||
public async Task Handle_DefaultsToDisabled()
|
||||
{
|
||||
var handler = new GetPlaybackPublicConfigQueryHandler(TestConfigurations.Playback());
|
||||
|
||||
var response = await handler.Handle(new GetPlaybackPublicConfigQuery(), TestContext.Current.CancellationToken);
|
||||
var response = await handler.Handle(new GetPlaybackPublicConfigQuery(), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.False(response.AllowAnonymousListening);
|
||||
}
|
||||
Assert.False(response.AllowAnonymousListening);
|
||||
}
|
||||
}
|
||||
|
||||
+20
-21
@@ -2,31 +2,30 @@ using Musify.Application.Genres;
|
||||
using Musify.Domain.ValueObjects;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Genres
|
||||
namespace Musify.Application.Tests.Genres;
|
||||
|
||||
public sealed class GetAvailableGenresQueryHandlerTests
|
||||
{
|
||||
public sealed class GetAvailableGenresQueryHandlerTests
|
||||
private static async Task<IReadOnlyList<AvailableGenreResponse>> RunAsync() =>
|
||||
await new GetAvailableGenresQueryHandler().Handle(new GetAvailableGenresQuery(), TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ReturnsEveryGenre()
|
||||
{
|
||||
private static async Task<IReadOnlyList<AvailableGenreResponse>> RunAsync() =>
|
||||
await new GetAvailableGenresQueryHandler().Handle(new GetAvailableGenresQuery(), TestContext.Current.CancellationToken);
|
||||
var result = await RunAsync();
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ReturnsEveryGenre()
|
||||
{
|
||||
var result = await RunAsync();
|
||||
Assert.Equal(Enum.GetValues<Genre>(), result.Select(r => r.Genre));
|
||||
}
|
||||
|
||||
Assert.Equal(Enum.GetValues<Genre>(), result.Select(r => r.Genre));
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_ListsIncompatibleGenresSymmetrically()
|
||||
{
|
||||
var result = await RunAsync();
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ListsIncompatibleGenresSymmetrically()
|
||||
{
|
||||
var result = await RunAsync();
|
||||
|
||||
var metal = result.Single(r => r.Genre == Genre.Metal);
|
||||
var lofi = result.Single(r => r.Genre == Genre.Lofi);
|
||||
Assert.Contains(Genre.Lofi, metal.IncompatibleWith);
|
||||
Assert.Contains(Genre.Metal, lofi.IncompatibleWith);
|
||||
Assert.Empty(result.Single(r => r.Genre == Genre.Pop).IncompatibleWith);
|
||||
}
|
||||
var metal = result.Single(r => r.Genre == Genre.Metal);
|
||||
var lofi = result.Single(r => r.Genre == Genre.Lofi);
|
||||
Assert.Contains(Genre.Lofi, metal.IncompatibleWith);
|
||||
Assert.Contains(Genre.Metal, lofi.IncompatibleWith);
|
||||
Assert.Empty(result.Single(r => r.Genre == Genre.Pop).IncompatibleWith);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,66 +4,65 @@ using Musify.Domain.Entities;
|
||||
using Musify.Domain.ValueObjects;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Genres
|
||||
namespace Musify.Application.Tests.Genres;
|
||||
|
||||
public sealed class GetGenresQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class GetGenresQueryHandlerTests : HandlerTestBase
|
||||
private GetGenresQueryHandler CreateHandler() => new(Database);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NoTracks_ReturnsEmpty()
|
||||
{
|
||||
private GetGenresQueryHandler CreateHandler() => new(Database);
|
||||
var result = await CreateHandler().Handle(new GetGenresQuery(), TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NoTracks_ReturnsEmpty()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new GetGenresQuery(), TestContext.Current.CancellationToken);
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
Assert.Empty(result);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_ReturnsOnlyGenresInUse_MostPopulatedFirst()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var rockA = TestEntities.Track(owner, "Rock A", tags: [Genre.Rock]);
|
||||
var rockB = TestEntities.Track(owner, "Rock B", tags: [Genre.Rock, Genre.Indie]);
|
||||
var jazz = TestEntities.Track(owner, "Jazz", tags: [Genre.Jazz]);
|
||||
await SeedAsync(
|
||||
owner, rockA, rockB, jazz,
|
||||
new UserHasTrack { UserId = owner.Id, TrackId = rockA.Id },
|
||||
new UserHasTrack { UserId = owner.Id, TrackId = rockB.Id },
|
||||
new UserHasTrack { UserId = owner.Id, TrackId = jazz.Id });
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ReturnsOnlyGenresInUse_MostPopulatedFirst()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var rockA = TestEntities.Track(owner, "Rock A", tags: [Genre.Rock]);
|
||||
var rockB = TestEntities.Track(owner, "Rock B", tags: [Genre.Rock, Genre.Indie]);
|
||||
var jazz = TestEntities.Track(owner, "Jazz", tags: [Genre.Jazz]);
|
||||
await SeedAsync(
|
||||
owner, rockA, rockB, jazz,
|
||||
new UserHasTrack { UserId = owner.Id, TrackId = rockA.Id },
|
||||
new UserHasTrack { UserId = owner.Id, TrackId = rockB.Id },
|
||||
new UserHasTrack { UserId = owner.Id, TrackId = jazz.Id });
|
||||
var result = await CreateHandler().Handle(new GetGenresQuery(), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetGenresQuery(), TestContext.Current.CancellationToken);
|
||||
Assert.Equal(
|
||||
[(Genre.Rock, 2), (Genre.Jazz, 1), (Genre.Indie, 1)],
|
||||
result.Select(r => (r.Genre, r.TrackCount)));
|
||||
}
|
||||
|
||||
Assert.Equal(
|
||||
[(Genre.Rock, 2), (Genre.Jazz, 1), (Genre.Indie, 1)],
|
||||
result.Select(r => (r.Genre, r.TrackCount)));
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_TrackInSeveralLibraries_IsCountedOnce()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var other = TestEntities.User(id: 2, name: "other-user");
|
||||
var track = TestEntities.Track(owner, tags: [Genre.Pop]);
|
||||
await SeedAsync(
|
||||
owner, other, track,
|
||||
new UserHasTrack { UserId = owner.Id, TrackId = track.Id },
|
||||
new UserHasTrack { UserId = other.Id, TrackId = track.Id });
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_TrackInSeveralLibraries_IsCountedOnce()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var other = TestEntities.User(id: 2, name: "other-user");
|
||||
var track = TestEntities.Track(owner, tags: [Genre.Pop]);
|
||||
await SeedAsync(
|
||||
owner, other, track,
|
||||
new UserHasTrack { UserId = owner.Id, TrackId = track.Id },
|
||||
new UserHasTrack { UserId = other.Id, TrackId = track.Id });
|
||||
var result = await CreateHandler().Handle(new GetGenresQuery(), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetGenresQuery(), TestContext.Current.CancellationToken);
|
||||
Assert.Equal(1, Assert.Single(result).TrackCount);
|
||||
}
|
||||
|
||||
Assert.Equal(1, Assert.Single(result).TrackCount);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_TrackNotInAnyLibrary_IsIgnored()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var track = TestEntities.Track(owner, tags: [Genre.Metal]);
|
||||
await SeedAsync(owner, track);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_TrackNotInAnyLibrary_IsIgnored()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var track = TestEntities.Track(owner, tags: [Genre.Metal]);
|
||||
await SeedAsync(owner, track);
|
||||
var result = await CreateHandler().Handle(new GetGenresQuery(), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetGenresQuery(), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Empty(result);
|
||||
}
|
||||
Assert.Empty(result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,53 +3,52 @@ using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Domain.Entities;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Likes
|
||||
namespace Musify.Application.Tests.Likes;
|
||||
|
||||
public sealed class GetLikedTracksQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class GetLikedTracksQueryHandlerTests : HandlerTestBase
|
||||
private GetLikedTracksQueryHandler CreateHandler() => new(Database);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_MultipleLikes_ReturnsMostRecentFirst()
|
||||
{
|
||||
private GetLikedTracksQueryHandler CreateHandler() => new(Database);
|
||||
var owner = TestEntities.User();
|
||||
var older = TestEntities.Track(owner, "Older like");
|
||||
var newer = TestEntities.Track(owner, "Newer like");
|
||||
await SeedAsync(
|
||||
owner, older, newer,
|
||||
new TrackLike { UserId = owner.Id, TrackId = older.Id, CreatedAt = DateTime.UtcNow.AddMinutes(-10) },
|
||||
new TrackLike { UserId = owner.Id, TrackId = newer.Id, CreatedAt = DateTime.UtcNow });
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_MultipleLikes_ReturnsMostRecentFirst()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var older = TestEntities.Track(owner, "Older like");
|
||||
var newer = TestEntities.Track(owner, "Newer like");
|
||||
await SeedAsync(
|
||||
owner, older, newer,
|
||||
new TrackLike { UserId = owner.Id, TrackId = older.Id, CreatedAt = DateTime.UtcNow.AddMinutes(-10) },
|
||||
new TrackLike { UserId = owner.Id, TrackId = newer.Id, CreatedAt = DateTime.UtcNow });
|
||||
var result = await CreateHandler().Handle(new GetLikedTracksQuery(owner.Id, 1, 10), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetLikedTracksQuery(owner.Id, 1, 10), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(["Newer like", "Older like"], result.Value.Items.Select(track => track.Title));
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(["Newer like", "Older like"], result.Value.Items.Select(track => track.Title));
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_NoLikes_ReturnsEmpty()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
await SeedAsync(owner);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NoLikes_ReturnsEmpty()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
await SeedAsync(owner);
|
||||
var result = await CreateHandler().Handle(new GetLikedTracksQuery(owner.Id, 1, 10), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetLikedTracksQuery(owner.Id, 1, 10), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Empty(result.Value.Items);
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Empty(result.Value.Items);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_OtherUsersLikes_AreExcluded()
|
||||
{
|
||||
var owner = TestEntities.User(1, "owner");
|
||||
var stranger = TestEntities.User(2, "stranger");
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(owner, stranger, track, new TrackLike { UserId = stranger.Id, TrackId = track.Id });
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_OtherUsersLikes_AreExcluded()
|
||||
{
|
||||
var owner = TestEntities.User(1, "owner");
|
||||
var stranger = TestEntities.User(2, "stranger");
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(owner, stranger, track, new TrackLike { UserId = stranger.Id, TrackId = track.Id });
|
||||
var result = await CreateHandler().Handle(new GetLikedTracksQuery(owner.Id, 1, 10), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetLikedTracksQuery(owner.Id, 1, 10), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Empty(result.Value.Items);
|
||||
}
|
||||
Assert.False(result.IsError);
|
||||
Assert.Empty(result.Value.Items);
|
||||
}
|
||||
}
|
||||
|
||||
+37
-38
@@ -4,53 +4,52 @@ using Musify.Application.Likes;
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Likes
|
||||
namespace Musify.Application.Tests.Likes;
|
||||
|
||||
public sealed class ToggleTrackLikeCommandHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class ToggleTrackLikeCommandHandlerTests : HandlerTestBase
|
||||
private ToggleTrackLikeCommandHandler CreateHandler() =>
|
||||
new(Database, NoOpLogger<ToggleTrackLikeCommandHandler>());
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_TrackNotLiked_LikesIt()
|
||||
{
|
||||
private ToggleTrackLikeCommandHandler CreateHandler() =>
|
||||
new(Database, NoOpLogger<ToggleTrackLikeCommandHandler>());
|
||||
var owner = TestEntities.User();
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(owner, track);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_TrackNotLiked_LikesIt()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(owner, track);
|
||||
var result = await CreateHandler().Handle(
|
||||
new ToggleTrackLikeCommand(owner.Id, track.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(
|
||||
new ToggleTrackLikeCommand(owner.Id, track.Id), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
Assert.True(result.Value);
|
||||
Assert.Single(await Database.TrackLikes.ToListAsync(TestContext.Current.CancellationToken));
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.True(result.Value);
|
||||
Assert.Single(await Database.TrackLikes.ToListAsync(TestContext.Current.CancellationToken));
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_TrackAlreadyLiked_UnlikesIt()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(owner, track, new Musify.Domain.Entities.TrackLike { UserId = owner.Id, TrackId = track.Id });
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_TrackAlreadyLiked_UnlikesIt()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(owner, track, new Musify.Domain.Entities.TrackLike { UserId = owner.Id, TrackId = track.Id });
|
||||
var result = await CreateHandler().Handle(
|
||||
new ToggleTrackLikeCommand(owner.Id, track.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(
|
||||
new ToggleTrackLikeCommand(owner.Id, track.Id), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
Assert.False(result.Value);
|
||||
Assert.Empty(await Database.TrackLikes.ToListAsync(TestContext.Current.CancellationToken));
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.False(result.Value);
|
||||
Assert.Empty(await Database.TrackLikes.ToListAsync(TestContext.Current.CancellationToken));
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_TrackMissing_ReturnsNotFound()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
await SeedAsync(owner);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_TrackMissing_ReturnsNotFound()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
await SeedAsync(owner);
|
||||
var result = await CreateHandler().Handle(
|
||||
new ToggleTrackLikeCommand(owner.Id, Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(
|
||||
new ToggleTrackLikeCommand(owner.Id, Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
}
|
||||
|
||||
+67
-68
@@ -4,89 +4,88 @@ using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Domain.Entities;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Mixes
|
||||
namespace Musify.Application.Tests.Mixes;
|
||||
|
||||
public sealed class GenerateMixesForUserCommandHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class GenerateMixesForUserCommandHandlerTests : HandlerTestBase
|
||||
private GenerateMixesForUserCommandHandler CreateHandler() => new(
|
||||
Database, TestConfigurations.Mix(), NoOpLogger<GenerateMixesForUserCommandHandler>());
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NoTracks_DoesNotCreateAnyMix()
|
||||
{
|
||||
private GenerateMixesForUserCommandHandler CreateHandler() => new(
|
||||
Database, TestConfigurations.Mix(), NoOpLogger<GenerateMixesForUserCommandHandler>());
|
||||
var user = TestEntities.User();
|
||||
await SeedAsync(user);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NoTracks_DoesNotCreateAnyMix()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
await SeedAsync(user);
|
||||
var result = await CreateHandler().Handle(new GenerateMixesForUserCommand(user.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GenerateMixesForUserCommand(user.Id), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Empty(await Database.Mixes.ToListAsync(TestContext.Current.CancellationToken));
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Empty(await Database.Mixes.ToListAsync(TestContext.Current.CancellationToken));
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_AllTracksAlreadyListenedTo_CreatesOnlyTheDailyMix()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
var track = TestEntities.Track(user);
|
||||
await SeedAsync(
|
||||
user, track,
|
||||
new UserHasTrack { UserId = user.Id, TrackId = track.Id },
|
||||
TestEntities.ListeningHistory(user.Id, track.Id));
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_AllTracksAlreadyListenedTo_CreatesOnlyTheDailyMix()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
var track = TestEntities.Track(user);
|
||||
await SeedAsync(
|
||||
user, track,
|
||||
new UserHasTrack { UserId = user.Id, TrackId = track.Id },
|
||||
TestEntities.ListeningHistory(user.Id, track.Id));
|
||||
var result = await CreateHandler().Handle(new GenerateMixesForUserCommand(user.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GenerateMixesForUserCommand(user.Id), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
var mixes = await Database.Mixes.ToListAsync(TestContext.Current.CancellationToken);
|
||||
Assert.Single(mixes);
|
||||
Assert.Equal("Tu mezcla diaria", mixes[0].Title);
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
var mixes = await Database.Mixes.ToListAsync(TestContext.Current.CancellationToken);
|
||||
Assert.Single(mixes);
|
||||
Assert.Equal("Tu mezcla diaria", mixes[0].Title);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_UnheardTracksInLibrary_CreatesDiscoveryAndDailyMixes_AndReplacesPreviousMixes()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
var listenedTrack = TestEntities.Track(user, "Listened Track");
|
||||
var unheardTrack = TestEntities.Track(user, "Unheard Track");
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UnheardTracksInLibrary_CreatesDiscoveryAndDailyMixes_AndReplacesPreviousMixes()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
var listenedTrack = TestEntities.Track(user, "Listened Track");
|
||||
var unheardTrack = TestEntities.Track(user, "Unheard Track");
|
||||
var staleMix = TestEntities.Mix(user.Id, "Stale Mix");
|
||||
var staleItem = TestEntities.MixItem(staleMix.Id, listenedTrack.Id);
|
||||
|
||||
var staleMix = TestEntities.Mix(user.Id, "Stale Mix");
|
||||
var staleItem = TestEntities.MixItem(staleMix.Id, listenedTrack.Id);
|
||||
await SeedAsync(
|
||||
user, listenedTrack, unheardTrack, staleMix, staleItem,
|
||||
new UserHasTrack { UserId = user.Id, TrackId = listenedTrack.Id },
|
||||
new UserHasTrack { UserId = user.Id, TrackId = unheardTrack.Id },
|
||||
TestEntities.ListeningHistory(user.Id, listenedTrack.Id));
|
||||
|
||||
await SeedAsync(
|
||||
user, listenedTrack, unheardTrack, staleMix, staleItem,
|
||||
new UserHasTrack { UserId = user.Id, TrackId = listenedTrack.Id },
|
||||
new UserHasTrack { UserId = user.Id, TrackId = unheardTrack.Id },
|
||||
TestEntities.ListeningHistory(user.Id, listenedTrack.Id));
|
||||
var result = await CreateHandler().Handle(new GenerateMixesForUserCommand(user.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GenerateMixesForUserCommand(user.Id), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
var mixes = await Database.Mixes.ToListAsync(TestContext.Current.CancellationToken);
|
||||
Assert.DoesNotContain(mixes, mix => mix.Title == "Stale Mix");
|
||||
Assert.Contains(mixes, mix => mix.Title == "Descubrimiento");
|
||||
Assert.Contains(mixes, mix => mix.Title == "Tu mezcla diaria");
|
||||
Assert.Empty(await Database.MixItems.Where(item => item.MixId == staleMix.Id).ToListAsync(TestContext.Current.CancellationToken));
|
||||
|
||||
Assert.False(result.IsError);
|
||||
var mixes = await Database.Mixes.ToListAsync(TestContext.Current.CancellationToken);
|
||||
Assert.DoesNotContain(mixes, mix => mix.Title == "Stale Mix");
|
||||
Assert.Contains(mixes, mix => mix.Title == "Descubrimiento");
|
||||
Assert.Contains(mixes, mix => mix.Title == "Tu mezcla diaria");
|
||||
Assert.Empty(await Database.MixItems.Where(item => item.MixId == staleMix.Id).ToListAsync(TestContext.Current.CancellationToken));
|
||||
var discoveryMix = mixes.Single(mix => mix.Title == "Descubrimiento");
|
||||
var discoveryItems = await Database.MixItems
|
||||
.Where(item => item.MixId == discoveryMix.Id)
|
||||
.ToListAsync(TestContext.Current.CancellationToken);
|
||||
Assert.All(discoveryItems, item => Assert.Equal(unheardTrack.Id, item.TrackId));
|
||||
}
|
||||
|
||||
var discoveryMix = mixes.Single(mix => mix.Title == "Descubrimiento");
|
||||
var discoveryItems = await Database.MixItems
|
||||
.Where(item => item.MixId == discoveryMix.Id)
|
||||
.ToListAsync(TestContext.Current.CancellationToken);
|
||||
Assert.All(discoveryItems, item => Assert.Equal(unheardTrack.Id, item.TrackId));
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_TrackWithOnlyUncountedListens_IsTreatedAsUnheard()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
var skipped = TestEntities.Track(user, "Only skipped");
|
||||
await SeedAsync(
|
||||
user, skipped,
|
||||
new UserHasTrack { UserId = user.Id, TrackId = skipped.Id },
|
||||
TestEntities.ListeningHistory(user.Id, skipped.Id, playedSeconds: 3, isCounted: false));
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_TrackWithOnlyUncountedListens_IsTreatedAsUnheard()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
var skipped = TestEntities.Track(user, "Only skipped");
|
||||
await SeedAsync(
|
||||
user, skipped,
|
||||
new UserHasTrack { UserId = user.Id, TrackId = skipped.Id },
|
||||
TestEntities.ListeningHistory(user.Id, skipped.Id, playedSeconds: 3, isCounted: false));
|
||||
await CreateHandler().Handle(new GenerateMixesForUserCommand(user.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
await CreateHandler().Handle(new GenerateMixesForUserCommand(user.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var mixes = await Database.Mixes.ToListAsync(TestContext.Current.CancellationToken);
|
||||
Assert.Contains(mixes, mix => mix.Title == "Descubrimiento");
|
||||
}
|
||||
var mixes = await Database.Mixes.ToListAsync(TestContext.Current.CancellationToken);
|
||||
Assert.Contains(mixes, mix => mix.Title == "Descubrimiento");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,48 +3,47 @@ using Musify.Application.Mixes;
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Mixes
|
||||
namespace Musify.Application.Tests.Mixes;
|
||||
|
||||
public sealed class GetMixByIdQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class GetMixByIdQueryHandlerTests : HandlerTestBase
|
||||
private GetMixByIdQueryHandler CreateHandler() => new(Database);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_OwnedMix_ReturnsItWithOrderedItems()
|
||||
{
|
||||
private GetMixByIdQueryHandler CreateHandler() => new(Database);
|
||||
var owner = TestEntities.User();
|
||||
var firstTrack = TestEntities.Track(owner, "First");
|
||||
var secondTrack = TestEntities.Track(owner, "Second");
|
||||
var mix = TestEntities.Mix(owner.Id, "Discovery");
|
||||
var second = TestEntities.MixItem(mix.Id, secondTrack.Id, position: 1);
|
||||
var first = TestEntities.MixItem(mix.Id, firstTrack.Id, position: 0);
|
||||
await SeedAsync(owner, firstTrack, secondTrack, mix, second, first);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_OwnedMix_ReturnsItWithOrderedItems()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var firstTrack = TestEntities.Track(owner, "First");
|
||||
var secondTrack = TestEntities.Track(owner, "Second");
|
||||
var mix = TestEntities.Mix(owner.Id, "Discovery");
|
||||
var second = TestEntities.MixItem(mix.Id, secondTrack.Id, position: 1);
|
||||
var first = TestEntities.MixItem(mix.Id, firstTrack.Id, position: 0);
|
||||
await SeedAsync(owner, firstTrack, secondTrack, mix, second, first);
|
||||
var result = await CreateHandler().Handle(new GetMixByIdQuery(owner.Id, mix.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetMixByIdQuery(owner.Id, mix.Id), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("Discovery", result.Value.Title);
|
||||
Assert.Equal(["First", "Second"], result.Value.Items.Select(item => item.Title));
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("Discovery", result.Value.Title);
|
||||
Assert.Equal(["First", "Second"], result.Value.Items.Select(item => item.Title));
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_MixMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new GetMixByIdQuery(1, Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_MixMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new GetMixByIdQuery(1, Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_MixBelongsToAnotherUser_ReturnsNotFound()
|
||||
{
|
||||
var owner = TestEntities.User(1, "owner");
|
||||
var mix = TestEntities.Mix(owner.Id);
|
||||
await SeedAsync(owner, mix);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_MixBelongsToAnotherUser_ReturnsNotFound()
|
||||
{
|
||||
var owner = TestEntities.User(1, "owner");
|
||||
var mix = TestEntities.Mix(owner.Id);
|
||||
await SeedAsync(owner, mix);
|
||||
var result = await CreateHandler().Handle(new GetMixByIdQuery(2, mix.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetMixByIdQuery(2, mix.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,38 +2,37 @@ using Musify.Application.Mixes;
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Mixes
|
||||
namespace Musify.Application.Tests.Mixes;
|
||||
|
||||
public sealed class GetMixesByUserIdQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class GetMixesByUserIdQueryHandlerTests : HandlerTestBase
|
||||
private GetMixesByUserIdQueryHandler CreateHandler() => new(Database);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ReturnsOnlyThatUsersMixesOrderedByPosition()
|
||||
{
|
||||
private GetMixesByUserIdQueryHandler CreateHandler() => new(Database);
|
||||
var owner = TestEntities.User(1, "owner");
|
||||
var other = TestEntities.User(2, "other");
|
||||
var second = TestEntities.Mix(owner.Id, "Second", position: 1);
|
||||
var first = TestEntities.Mix(owner.Id, "First", position: 0);
|
||||
var theirs = TestEntities.Mix(other.Id, "Theirs");
|
||||
await SeedAsync(owner, other, second, first, theirs);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ReturnsOnlyThatUsersMixesOrderedByPosition()
|
||||
{
|
||||
var owner = TestEntities.User(1, "owner");
|
||||
var other = TestEntities.User(2, "other");
|
||||
var second = TestEntities.Mix(owner.Id, "Second", position: 1);
|
||||
var first = TestEntities.Mix(owner.Id, "First", position: 0);
|
||||
var theirs = TestEntities.Mix(other.Id, "Theirs");
|
||||
await SeedAsync(owner, other, second, first, theirs);
|
||||
var result = await CreateHandler().Handle(new GetMixesByUserIdQuery(owner.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetMixesByUserIdQuery(owner.Id), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(["First", "Second"], result.Value.Select(mix => mix.Title));
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(["First", "Second"], result.Value.Select(mix => mix.Title));
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_NoMixes_ReturnsEmptyList()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
await SeedAsync(owner);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NoMixes_ReturnsEmptyList()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
await SeedAsync(owner);
|
||||
var result = await CreateHandler().Handle(new GetMixesByUserIdQuery(owner.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetMixesByUserIdQuery(owner.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Empty(result.Value);
|
||||
}
|
||||
Assert.False(result.IsError);
|
||||
Assert.Empty(result.Value);
|
||||
}
|
||||
}
|
||||
|
||||
+65
-66
@@ -5,89 +5,88 @@ using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Domain.Entities;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.PlayLists
|
||||
namespace Musify.Application.Tests.PlayLists;
|
||||
|
||||
public sealed class AddTrackToPlayListCommandHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class AddTrackToPlayListCommandHandlerTests : HandlerTestBase
|
||||
private AddTrackToPlayListCommandHandler CreateHandler() => new(Database, NoOpLogger<AddTrackToPlayListCommandHandler>());
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_FirstTrack_AddsItAtPositionZero()
|
||||
{
|
||||
private AddTrackToPlayListCommandHandler CreateHandler() => new(Database, NoOpLogger<AddTrackToPlayListCommandHandler>());
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(owner, playList, track);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_FirstTrack_AddsItAtPositionZero()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(owner, playList, track);
|
||||
var result = await CreateHandler().Handle(new AddTrackToPlayListCommand(owner.Id, playList.Id, track.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new AddTrackToPlayListCommand(owner.Id, playList.Id, track.Id), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
var link = Assert.Single(await Database.PlayListHasTracks.ToListAsync(TestContext.Current.CancellationToken));
|
||||
Assert.Equal(0, link.Position);
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
var link = Assert.Single(await Database.PlayListHasTracks.ToListAsync(TestContext.Current.CancellationToken));
|
||||
Assert.Equal(0, link.Position);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_PlayListAlreadyHasTracks_AppendsAtNextPosition()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
var existingTrack = TestEntities.Track(owner, "Existing");
|
||||
var newTrack = TestEntities.Track(owner, "New");
|
||||
await SeedAsync(
|
||||
owner, playList, existingTrack, newTrack,
|
||||
new PlayListHasTrack { PlayListId = playList.Id, TrackId = existingTrack.Id, Position = 0 });
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_PlayListAlreadyHasTracks_AppendsAtNextPosition()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
var existingTrack = TestEntities.Track(owner, "Existing");
|
||||
var newTrack = TestEntities.Track(owner, "New");
|
||||
await SeedAsync(
|
||||
owner, playList, existingTrack, newTrack,
|
||||
new PlayListHasTrack { PlayListId = playList.Id, TrackId = existingTrack.Id, Position = 0 });
|
||||
var result = await CreateHandler().Handle(new AddTrackToPlayListCommand(owner.Id, playList.Id, newTrack.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new AddTrackToPlayListCommand(owner.Id, playList.Id, newTrack.Id), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
var newLink = await Database.PlayListHasTracks.SingleAsync(link => link.TrackId == newTrack.Id, TestContext.Current.CancellationToken);
|
||||
Assert.Equal(1, newLink.Position);
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
var newLink = await Database.PlayListHasTracks.SingleAsync(link => link.TrackId == newTrack.Id, TestContext.Current.CancellationToken);
|
||||
Assert.Equal(1, newLink.Position);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_PlayListMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new AddTrackToPlayListCommand(1, Guid.NewGuid(), Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_PlayListMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new AddTrackToPlayListCommand(1, Guid.NewGuid(), Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_NotOwner_ReturnsUnauthorized()
|
||||
{
|
||||
var owner = TestEntities.User(1, "owner");
|
||||
var stranger = TestEntities.User(2, "stranger");
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
await SeedAsync(owner, stranger, playList);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NotOwner_ReturnsUnauthorized()
|
||||
{
|
||||
var owner = TestEntities.User(1, "owner");
|
||||
var stranger = TestEntities.User(2, "stranger");
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
await SeedAsync(owner, stranger, playList);
|
||||
var result = await CreateHandler().Handle(new AddTrackToPlayListCommand(stranger.Id, playList.Id, Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new AddTrackToPlayListCommand(stranger.Id, playList.Id, Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
|
||||
}
|
||||
|
||||
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_TrackMissing_ReturnsNotFound()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
await SeedAsync(owner, playList);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_TrackMissing_ReturnsNotFound()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
await SeedAsync(owner, playList);
|
||||
var result = await CreateHandler().Handle(new AddTrackToPlayListCommand(owner.Id, playList.Id, Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new AddTrackToPlayListCommand(owner.Id, playList.Id, Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_TrackAlreadyInPlayList_ReturnsConflict()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(owner, playList, track, new PlayListHasTrack { PlayListId = playList.Id, TrackId = track.Id, Position = 0 });
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_TrackAlreadyInPlayList_ReturnsConflict()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(owner, playList, track, new PlayListHasTrack { PlayListId = playList.Id, TrackId = track.Id, Position = 0 });
|
||||
var result = await CreateHandler().Handle(new AddTrackToPlayListCommand(owner.Id, playList.Id, track.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new AddTrackToPlayListCommand(owner.Id, playList.Id, track.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
|
||||
}
|
||||
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
|
||||
}
|
||||
}
|
||||
|
||||
+78
-79
@@ -6,109 +6,108 @@ using Musify.Application.Tests.TestSupport;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.PlayLists
|
||||
namespace Musify.Application.Tests.PlayLists;
|
||||
|
||||
public sealed class CreatePlayListCommandHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class CreatePlayListCommandHandlerTests : HandlerTestBase
|
||||
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
|
||||
private readonly IStorageService storageService = Substitute.For<IStorageService>();
|
||||
|
||||
private CreatePlayListCommandHandler CreateHandler() => new(
|
||||
eventBus,
|
||||
Database,
|
||||
new UploadIntentValidator(Database, storageService),
|
||||
NoOpLogger<CreatePlayListCommandHandler>(),
|
||||
TestConfigurations.Storage(),
|
||||
TestConfigurations.PlayList(),
|
||||
TestConfigurations.UploadIntent());
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NoPictureIntent_CreatesPlayListWithoutPictures()
|
||||
{
|
||||
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
|
||||
private readonly IStorageService storageService = Substitute.For<IStorageService>();
|
||||
var user = TestEntities.User();
|
||||
await SeedAsync(user);
|
||||
|
||||
private CreatePlayListCommandHandler CreateHandler() => new(
|
||||
eventBus,
|
||||
Database,
|
||||
new UploadIntentValidator(Database, storageService),
|
||||
NoOpLogger<CreatePlayListCommandHandler>(),
|
||||
TestConfigurations.Storage(),
|
||||
TestConfigurations.PlayList(),
|
||||
TestConfigurations.UploadIntent());
|
||||
var command = new CreatePlayListCommand(user.Id, "My Playlist", "A description", PictureIntentId: null);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NoPictureIntent_CreatesPlayListWithoutPictures()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
await SeedAsync(user);
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
var command = new CreatePlayListCommand(user.Id, "My Playlist", "A description", PictureIntentId: null);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("My Playlist", result.Value.Name);
|
||||
Assert.Null(result.Value.SmallImageKeyName);
|
||||
await eventBus.DidNotReceive().PublishAsync(Arg.Any<Musify.Application.Events.CreatePlayListResourcesEvent>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task Handle_WithValidPictureIntent_PublishesResourcesEvent()
|
||||
{
|
||||
storageService
|
||||
.HeadObjectAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new ObjectMetaData("image/webp", 1024));
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("My Playlist", result.Value.Name);
|
||||
Assert.Null(result.Value.SmallImageKeyName);
|
||||
await eventBus.DidNotReceive().PublishAsync(Arg.Any<Musify.Application.Events.CreatePlayListResourcesEvent>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
var user = TestEntities.User();
|
||||
var intent = TestEntities.UploadIntent(user.Id, Musify.Domain.ValueObjects.UploadIntentPurpose.PlayListPicture, objectName: "cover.webp");
|
||||
await SeedAsync(user, intent);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WithValidPictureIntent_PublishesResourcesEvent()
|
||||
{
|
||||
storageService
|
||||
.HeadObjectAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new ObjectMetaData("image/webp", 1024));
|
||||
var command = new CreatePlayListCommand(user.Id, "My Playlist", null, intent.Id);
|
||||
|
||||
var user = TestEntities.User();
|
||||
var intent = TestEntities.UploadIntent(user.Id, Musify.Domain.ValueObjects.UploadIntentPurpose.PlayListPicture, objectName: "cover.webp");
|
||||
await SeedAsync(user, intent);
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
var command = new CreatePlayListCommand(user.Id, "My Playlist", null, intent.Id);
|
||||
Assert.False(result.IsError);
|
||||
var stored = await Database.PlayLists.FindAsync([result.Value.Id], TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(stored);
|
||||
Assert.Equal("cover.webp", stored.Pictures?.OriginalName);
|
||||
await eventBus.Received(1).PublishAsync(Arg.Any<Musify.Application.Events.CreatePlayListResourcesEvent>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task Handle_NoVisibilitySpecified_DefaultsToPrivate()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
await SeedAsync(user);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
var stored = await Database.PlayLists.FindAsync([result.Value.Id], TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(stored);
|
||||
Assert.Equal("cover.webp", stored.Pictures?.OriginalName);
|
||||
await eventBus.Received(1).PublishAsync(Arg.Any<Musify.Application.Events.CreatePlayListResourcesEvent>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
var command = new CreatePlayListCommand(user.Id, "My Playlist", null, PictureIntentId: null);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NoVisibilitySpecified_DefaultsToPrivate()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
await SeedAsync(user);
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
var command = new CreatePlayListCommand(user.Id, "My Playlist", null, PictureIntentId: null);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(Musify.Domain.ValueObjects.PlaylistVisibility.Private, result.Value.Visibility);
|
||||
}
|
||||
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task Handle_PublicVisibility_PersistsIt()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
await SeedAsync(user);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(Musify.Domain.ValueObjects.PlaylistVisibility.Private, result.Value.Visibility);
|
||||
}
|
||||
var command = new CreatePlayListCommand(user.Id, "My Playlist", null, PictureIntentId: null, Visibility: Musify.Domain.ValueObjects.PlaylistVisibility.Public);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_PublicVisibility_PersistsIt()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
await SeedAsync(user);
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
var command = new CreatePlayListCommand(user.Id, "My Playlist", null, PictureIntentId: null, Visibility: Musify.Domain.ValueObjects.PlaylistVisibility.Public);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(Musify.Domain.ValueObjects.PlaylistVisibility.Public, result.Value.Visibility);
|
||||
}
|
||||
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task Handle_UserMissing_ReturnsNotFound()
|
||||
{
|
||||
var command = new CreatePlayListCommand(404, "Orphan Playlist", null, null);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(Musify.Domain.ValueObjects.PlaylistVisibility.Public, result.Value.Visibility);
|
||||
}
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UserMissing_ReturnsNotFound()
|
||||
{
|
||||
var command = new CreatePlayListCommand(404, "Orphan Playlist", null, null);
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task Handle_PictureIntentNotFound_ReturnsError()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
await SeedAsync(user);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
var command = new CreatePlayListCommand(user.Id, "My Playlist", null, Guid.NewGuid());
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_PictureIntentNotFound_ReturnsError()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
await SeedAsync(user);
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
var command = new CreatePlayListCommand(user.Id, "My Playlist", null, Guid.NewGuid());
|
||||
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.True(result.IsError);
|
||||
}
|
||||
Assert.True(result.IsError);
|
||||
}
|
||||
}
|
||||
|
||||
+34
-35
@@ -6,49 +6,48 @@ using Musify.Domain.ValueObjects;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.PlayLists
|
||||
namespace Musify.Application.Tests.PlayLists;
|
||||
|
||||
public sealed class DeletePlayListCommandHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class DeletePlayListCommandHandlerTests : HandlerTestBase
|
||||
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
|
||||
|
||||
private DeletePlayListCommandHandler CreateHandler() => new(Database, eventBus, NoOpLogger<DeletePlayListCommandHandler>());
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_Owner_MarksPlayListAsRemoving()
|
||||
{
|
||||
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
await SeedAsync(owner, playList);
|
||||
|
||||
private DeletePlayListCommandHandler CreateHandler() => new(Database, eventBus, NoOpLogger<DeletePlayListCommandHandler>());
|
||||
var result = await CreateHandler().Handle(new DeletePlayListCommand(owner.Id, playList.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_Owner_MarksPlayListAsRemoving()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
await SeedAsync(owner, playList);
|
||||
Assert.False(result.IsError);
|
||||
var stored = await Database.PlayLists.FindAsync([playList.Id], TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(stored);
|
||||
Assert.Equal(LifeCycleStatus.Removing, stored.LifeCycleStatus);
|
||||
await eventBus.Received(1).PublishAsync(Arg.Any<Musify.Application.Events.DeletePlayListEvent>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
var result = await CreateHandler().Handle(new DeletePlayListCommand(owner.Id, playList.Id), TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task Handle_PlayListMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new DeletePlayListCommand(1, Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
var stored = await Database.PlayLists.FindAsync([playList.Id], TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(stored);
|
||||
Assert.Equal(LifeCycleStatus.Removing, stored.LifeCycleStatus);
|
||||
await eventBus.Received(1).PublishAsync(Arg.Any<Musify.Application.Events.DeletePlayListEvent>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_PlayListMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new DeletePlayListCommand(1, Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task Handle_NotOwner_ReturnsUnauthorized()
|
||||
{
|
||||
var owner = TestEntities.User(1, "owner");
|
||||
var stranger = TestEntities.User(2, "stranger");
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
await SeedAsync(owner, stranger, playList);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
var result = await CreateHandler().Handle(new DeletePlayListCommand(stranger.Id, playList.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NotOwner_ReturnsUnauthorized()
|
||||
{
|
||||
var owner = TestEntities.User(1, "owner");
|
||||
var stranger = TestEntities.User(2, "stranger");
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
await SeedAsync(owner, stranger, playList);
|
||||
|
||||
var result = await CreateHandler().Handle(new DeletePlayListCommand(stranger.Id, playList.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
|
||||
}
|
||||
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
|
||||
}
|
||||
}
|
||||
|
||||
+58
-59
@@ -4,82 +4,81 @@ using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Domain.Entities;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.PlayLists
|
||||
namespace Musify.Application.Tests.PlayLists;
|
||||
|
||||
public sealed class GetPlayListByIdQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class GetPlayListByIdQueryHandlerTests : HandlerTestBase
|
||||
private GetPlayListByIdQueryHandler CreateHandler() => new(Database);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ExistingPlayList_ReturnsItWithCoverTrackIds()
|
||||
{
|
||||
private GetPlayListByIdQueryHandler CreateHandler() => new(Database);
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id, visibility: Musify.Domain.ValueObjects.PlaylistVisibility.Public);
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(owner, playList, track, new PlayListHasTrack { PlayListId = playList.Id, TrackId = track.Id, Position = 0 });
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ExistingPlayList_ReturnsItWithCoverTrackIds()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id, visibility: Musify.Domain.ValueObjects.PlaylistVisibility.Public);
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(owner, playList, track, new PlayListHasTrack { PlayListId = playList.Id, TrackId = track.Id, Position = 0 });
|
||||
var result = await CreateHandler().Handle(new GetPlayListByIdQuery(playList.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetPlayListByIdQuery(playList.Id), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(playList.Name, result.Value.Name);
|
||||
Assert.Equal([track.Id], result.Value.CoverTrackIds);
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(playList.Name, result.Value.Name);
|
||||
Assert.Equal([track.Id], result.Value.CoverTrackIds);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_PlayListMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new GetPlayListByIdQuery(Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_PlayListMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new GetPlayListByIdQuery(Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_PrivatePlayList_RequestedByOwner_ReturnsIt()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id, visibility: Musify.Domain.ValueObjects.PlaylistVisibility.Private);
|
||||
await SeedAsync(owner, playList);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_PrivatePlayList_RequestedByOwner_ReturnsIt()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id, visibility: Musify.Domain.ValueObjects.PlaylistVisibility.Private);
|
||||
await SeedAsync(owner, playList);
|
||||
var result = await CreateHandler().Handle(new GetPlayListByIdQuery(playList.Id, owner.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetPlayListByIdQuery(playList.Id, owner.Id), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_PrivatePlayList_RequestedByStranger_ReturnsNotFound()
|
||||
{
|
||||
var owner = TestEntities.User(1, "owner");
|
||||
var stranger = TestEntities.User(2, "stranger");
|
||||
var playList = TestEntities.PlayList(owner.Id, visibility: Musify.Domain.ValueObjects.PlaylistVisibility.Private);
|
||||
await SeedAsync(owner, stranger, playList);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_PrivatePlayList_RequestedByStranger_ReturnsNotFound()
|
||||
{
|
||||
var owner = TestEntities.User(1, "owner");
|
||||
var stranger = TestEntities.User(2, "stranger");
|
||||
var playList = TestEntities.PlayList(owner.Id, visibility: Musify.Domain.ValueObjects.PlaylistVisibility.Private);
|
||||
await SeedAsync(owner, stranger, playList);
|
||||
var result = await CreateHandler().Handle(new GetPlayListByIdQuery(playList.Id, stranger.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetPlayListByIdQuery(playList.Id, stranger.Id), TestContext.Current.CancellationToken);
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_PrivatePlayList_RequestedAnonymously_ReturnsNotFound()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id, visibility: Musify.Domain.ValueObjects.PlaylistVisibility.Private);
|
||||
await SeedAsync(owner, playList);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_PrivatePlayList_RequestedAnonymously_ReturnsNotFound()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id, visibility: Musify.Domain.ValueObjects.PlaylistVisibility.Private);
|
||||
await SeedAsync(owner, playList);
|
||||
var result = await CreateHandler().Handle(new GetPlayListByIdQuery(playList.Id, RequestingUserId: null), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetPlayListByIdQuery(playList.Id, RequestingUserId: null), TestContext.Current.CancellationToken);
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_PublicPlayList_RequestedAnonymously_ReturnsIt()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id, visibility: Musify.Domain.ValueObjects.PlaylistVisibility.Public);
|
||||
await SeedAsync(owner, playList);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_PublicPlayList_RequestedAnonymously_ReturnsIt()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id, visibility: Musify.Domain.ValueObjects.PlaylistVisibility.Public);
|
||||
await SeedAsync(owner, playList);
|
||||
var result = await CreateHandler().Handle(new GetPlayListByIdQuery(playList.Id, RequestingUserId: null), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetPlayListByIdQuery(playList.Id, RequestingUserId: null), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
}
|
||||
Assert.False(result.IsError);
|
||||
}
|
||||
}
|
||||
|
||||
+34
-35
@@ -3,49 +3,48 @@ using Musify.Application.PlayLists;
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.PlayLists
|
||||
namespace Musify.Application.Tests.PlayLists;
|
||||
|
||||
public sealed class GetPlayListCoverQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class GetPlayListCoverQueryHandlerTests : HandlerTestBase
|
||||
private GetPlayListCoverQueryHandler CreateHandler() =>
|
||||
new(Database, TestConfigurations.Storage("covers-bucket"), TestConfigurations.PlayList());
|
||||
|
||||
[Theory]
|
||||
[InlineData("small", "small.webp")]
|
||||
[InlineData("large", "large.webp")]
|
||||
[InlineData("medium", "medium.webp")]
|
||||
public async Task Handle_ExistingPlayList_ReturnsRequestedSize(string size, string expectedFileName)
|
||||
{
|
||||
private GetPlayListCoverQueryHandler CreateHandler() =>
|
||||
new(Database, TestConfigurations.Storage("covers-bucket"), TestConfigurations.PlayList());
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
await SeedAsync(owner, playList);
|
||||
|
||||
[Theory]
|
||||
[InlineData("small", "small.webp")]
|
||||
[InlineData("large", "large.webp")]
|
||||
[InlineData("medium", "medium.webp")]
|
||||
public async Task Handle_ExistingPlayList_ReturnsRequestedSize(string size, string expectedFileName)
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
await SeedAsync(owner, playList);
|
||||
var result = await CreateHandler().Handle(new GetPlayListCoverQuery(playList.Id, size), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetPlayListCoverQuery(playList.Id, size), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("covers-bucket", result.Value.Bucket);
|
||||
Assert.EndsWith(expectedFileName, result.Value.Key);
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("covers-bucket", result.Value.Bucket);
|
||||
Assert.EndsWith(expectedFileName, result.Value.Key);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_PlayListMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new GetPlayListCoverQuery(Guid.NewGuid(), "small"), TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_PlayListMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new GetPlayListCoverQuery(Guid.NewGuid(), "small"), TestContext.Current.CancellationToken);
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_PlayListWithoutPictures_ReturnsNotFound()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
playList.Pictures = null;
|
||||
await SeedAsync(owner, playList);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_PlayListWithoutPictures_ReturnsNotFound()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
playList.Pictures = null;
|
||||
await SeedAsync(owner, playList);
|
||||
var result = await CreateHandler().Handle(new GetPlayListCoverQuery(playList.Id, "small"), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetPlayListCoverQuery(playList.Id, "small"), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
}
|
||||
|
||||
+24
-25
@@ -4,36 +4,35 @@ using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Domain.Entities;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.PlayLists
|
||||
namespace Musify.Application.Tests.PlayLists;
|
||||
|
||||
public sealed class GetPlayListTracksQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class GetPlayListTracksQueryHandlerTests : HandlerTestBase
|
||||
private GetPlayListTracksQueryHandler CreateHandler() => new(Database);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_TracksOrderedByPosition_ReturnsThemInOrder()
|
||||
{
|
||||
private GetPlayListTracksQueryHandler CreateHandler() => new(Database);
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
var second = TestEntities.Track(owner, "Second");
|
||||
var first = TestEntities.Track(owner, "First");
|
||||
await SeedAsync(
|
||||
owner, playList, second, first,
|
||||
new PlayListHasTrack { PlayListId = playList.Id, TrackId = second.Id, Position = 1 },
|
||||
new PlayListHasTrack { PlayListId = playList.Id, TrackId = first.Id, Position = 0 });
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_TracksOrderedByPosition_ReturnsThemInOrder()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
var second = TestEntities.Track(owner, "Second");
|
||||
var first = TestEntities.Track(owner, "First");
|
||||
await SeedAsync(
|
||||
owner, playList, second, first,
|
||||
new PlayListHasTrack { PlayListId = playList.Id, TrackId = second.Id, Position = 1 },
|
||||
new PlayListHasTrack { PlayListId = playList.Id, TrackId = first.Id, Position = 0 });
|
||||
var result = await CreateHandler().Handle(new GetPlayListTracksQuery(playList.Id, PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetPlayListTracksQuery(playList.Id, PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(["First", "Second"], result.Value.Items.Select(track => track.Title));
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(["First", "Second"], result.Value.Items.Select(track => track.Title));
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_PlayListMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new GetPlayListTracksQuery(Guid.NewGuid(), PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_PlayListMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new GetPlayListTracksQuery(Guid.NewGuid(), PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
}
|
||||
|
||||
+41
-42
@@ -3,57 +3,56 @@ using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Domain.ValueObjects;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.PlayLists
|
||||
namespace Musify.Application.Tests.PlayLists;
|
||||
|
||||
public sealed class GetPlayListsByUserIdQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class GetPlayListsByUserIdQueryHandlerTests : HandlerTestBase
|
||||
private GetPlayListsByUserIdQueryHandler CreateHandler() => new(Database);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ReturnsOnlyActivePlayListsForThatUser()
|
||||
{
|
||||
private GetPlayListsByUserIdQueryHandler CreateHandler() => new(Database);
|
||||
var owner = TestEntities.User(1, "owner");
|
||||
var other = TestEntities.User(2, "other");
|
||||
await SeedAsync(
|
||||
owner, other,
|
||||
TestEntities.PlayList(owner.Id, "Mine"),
|
||||
TestEntities.PlayList(owner.Id, "Being removed", lifeCycleStatus: LifeCycleStatus.Removing),
|
||||
TestEntities.PlayList(other.Id, "Theirs"));
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ReturnsOnlyActivePlayListsForThatUser()
|
||||
{
|
||||
var owner = TestEntities.User(1, "owner");
|
||||
var other = TestEntities.User(2, "other");
|
||||
await SeedAsync(
|
||||
owner, other,
|
||||
TestEntities.PlayList(owner.Id, "Mine"),
|
||||
TestEntities.PlayList(owner.Id, "Being removed", lifeCycleStatus: LifeCycleStatus.Removing),
|
||||
TestEntities.PlayList(other.Id, "Theirs"));
|
||||
var result = await CreateHandler().Handle(new GetPlayListsByUserIdQuery(owner.Id, Name: null, PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetPlayListsByUserIdQuery(owner.Id, Name: null, PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
var playList = Assert.Single(result.Value.Items);
|
||||
Assert.Equal("Mine", playList.Name);
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
var playList = Assert.Single(result.Value.Items);
|
||||
Assert.Equal("Mine", playList.Name);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_OnlyPublic_ReturnsOnlyPublicPlayLists()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
await SeedAsync(
|
||||
owner,
|
||||
TestEntities.PlayList(owner.Id, "Public one", visibility: PlaylistVisibility.Public),
|
||||
TestEntities.PlayList(owner.Id, "Private one", visibility: PlaylistVisibility.Private));
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_OnlyPublic_ReturnsOnlyPublicPlayLists()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
await SeedAsync(
|
||||
owner,
|
||||
TestEntities.PlayList(owner.Id, "Public one", visibility: PlaylistVisibility.Public),
|
||||
TestEntities.PlayList(owner.Id, "Private one", visibility: PlaylistVisibility.Private));
|
||||
var result = await CreateHandler().Handle(new GetPlayListsByUserIdQuery(owner.Id, Name: null, PageNumber: 1, PageSize: 10, OnlyPublic: true), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetPlayListsByUserIdQuery(owner.Id, Name: null, PageNumber: 1, PageSize: 10, OnlyPublic: true), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
var playList = Assert.Single(result.Value.Items);
|
||||
Assert.Equal("Public one", playList.Name);
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
var playList = Assert.Single(result.Value.Items);
|
||||
Assert.Equal("Public one", playList.Name);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_NameFilter_ReturnsOnlyMatchingPlayLists()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
await SeedAsync(owner, TestEntities.PlayList(owner.Id, "Road Trip"), TestEntities.PlayList(owner.Id, "Study Focus"));
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NameFilter_ReturnsOnlyMatchingPlayLists()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
await SeedAsync(owner, TestEntities.PlayList(owner.Id, "Road Trip"), TestEntities.PlayList(owner.Id, "Study Focus"));
|
||||
var result = await CreateHandler().Handle(new GetPlayListsByUserIdQuery(owner.Id, Name: "road", PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetPlayListsByUserIdQuery(owner.Id, Name: "road", PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
var playList = Assert.Single(result.Value.Items);
|
||||
Assert.Equal("Road Trip", playList.Name);
|
||||
}
|
||||
Assert.False(result.IsError);
|
||||
var playList = Assert.Single(result.Value.Items);
|
||||
Assert.Equal("Road Trip", playList.Name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,31 +2,30 @@ using Musify.Application.PlayLists;
|
||||
using Musify.Application.Tests.TestSupport;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.PlayLists
|
||||
namespace Musify.Application.Tests.PlayLists;
|
||||
|
||||
public sealed class GetPlayListsQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class GetPlayListsQueryHandlerTests : HandlerTestBase
|
||||
private GetPlayListsQueryHandler CreateHandler() => new(Database);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ReturnsEveryPlayListPaged()
|
||||
{
|
||||
private GetPlayListsQueryHandler CreateHandler() => new(Database);
|
||||
var owner = TestEntities.User();
|
||||
await SeedAsync(owner, TestEntities.PlayList(owner.Id, "One"), TestEntities.PlayList(owner.Id, "Two"));
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ReturnsEveryPlayListPaged()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
await SeedAsync(owner, TestEntities.PlayList(owner.Id, "One"), TestEntities.PlayList(owner.Id, "Two"));
|
||||
var result = await CreateHandler().Handle(new GetPlayListsQuery(PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetPlayListsQuery(PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(2, result.Value.TotalItemCount);
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(2, result.Value.TotalItemCount);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_NoPlayLists_ReturnsEmptyPage()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new GetPlayListsQuery(PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NoPlayLists_ReturnsEmptyPage()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new GetPlayListsQuery(PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Empty(result.Value.Items);
|
||||
}
|
||||
Assert.False(result.IsError);
|
||||
Assert.Empty(result.Value.Items);
|
||||
}
|
||||
}
|
||||
|
||||
+39
-40
@@ -4,57 +4,56 @@ using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Domain.Entities;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.PlayLists
|
||||
namespace Musify.Application.Tests.PlayLists;
|
||||
|
||||
public sealed class RemoveTrackFromPlayListCommandHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class RemoveTrackFromPlayListCommandHandlerTests : HandlerTestBase
|
||||
private RemoveTrackFromPlayListCommandHandler CreateHandler() => new(Database, NoOpLogger<RemoveTrackFromPlayListCommandHandler>());
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_TrackInPlayList_RemovesTheLink()
|
||||
{
|
||||
private RemoveTrackFromPlayListCommandHandler CreateHandler() => new(Database, NoOpLogger<RemoveTrackFromPlayListCommandHandler>());
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(owner, playList, track, new PlayListHasTrack { PlayListId = playList.Id, TrackId = track.Id, Position = 0 });
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_TrackInPlayList_RemovesTheLink()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(owner, playList, track, new PlayListHasTrack { PlayListId = playList.Id, TrackId = track.Id, Position = 0 });
|
||||
var result = await CreateHandler().Handle(new RemoveTrackFromPlayListCommand(owner.Id, playList.Id, track.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new RemoveTrackFromPlayListCommand(owner.Id, playList.Id, track.Id), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Empty(Database.PlayListHasTracks);
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Empty(Database.PlayListHasTracks);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_PlayListMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new RemoveTrackFromPlayListCommand(1, Guid.NewGuid(), Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_PlayListMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new RemoveTrackFromPlayListCommand(1, Guid.NewGuid(), Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_NotOwner_ReturnsUnauthorized()
|
||||
{
|
||||
var owner = TestEntities.User(1, "owner");
|
||||
var stranger = TestEntities.User(2, "stranger");
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
await SeedAsync(owner, stranger, playList);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NotOwner_ReturnsUnauthorized()
|
||||
{
|
||||
var owner = TestEntities.User(1, "owner");
|
||||
var stranger = TestEntities.User(2, "stranger");
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
await SeedAsync(owner, stranger, playList);
|
||||
var result = await CreateHandler().Handle(new RemoveTrackFromPlayListCommand(stranger.Id, playList.Id, Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new RemoveTrackFromPlayListCommand(stranger.Id, playList.Id, Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
|
||||
}
|
||||
|
||||
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_TrackNotLinked_ReturnsNotFound()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
await SeedAsync(owner, playList);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_TrackNotLinked_ReturnsNotFound()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
await SeedAsync(owner, playList);
|
||||
var result = await CreateHandler().Handle(new RemoveTrackFromPlayListCommand(owner.Id, playList.Id, Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new RemoveTrackFromPlayListCommand(owner.Id, playList.Id, Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
}
|
||||
|
||||
+47
-48
@@ -8,68 +8,67 @@ using Musify.Application.Tests.TestSupport;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.PlayLists
|
||||
namespace Musify.Application.Tests.PlayLists;
|
||||
|
||||
public sealed class RequestPlayListPictureUploadCommandHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class RequestPlayListPictureUploadCommandHandlerTests : HandlerTestBase
|
||||
private readonly IStorageService storageService = Substitute.For<IStorageService>();
|
||||
|
||||
private RequestPlayListPictureUploadCommandHandler CreateHandler(UploadIntentConfiguration? uploadIntentConfig = null)
|
||||
{
|
||||
private readonly IStorageService storageService = Substitute.For<IStorageService>();
|
||||
storageService
|
||||
.GetUploadUrlAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<TimeSpan>(), Arg.Any<CancellationToken>())
|
||||
.Returns("https://storage.musify.test/presigned-upload");
|
||||
|
||||
private RequestPlayListPictureUploadCommandHandler CreateHandler(UploadIntentConfiguration? uploadIntentConfig = null)
|
||||
{
|
||||
storageService
|
||||
.GetUploadUrlAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<TimeSpan>(), Arg.Any<CancellationToken>())
|
||||
.Returns("https://storage.musify.test/presigned-upload");
|
||||
return new RequestPlayListPictureUploadCommandHandler(
|
||||
Database,
|
||||
new UploadIntentValidator(Database, storageService),
|
||||
storageService,
|
||||
NoOpLogger<RequestPlayListPictureUploadCommandHandler>(),
|
||||
TestConfigurations.Storage(),
|
||||
TestConfigurations.PlayList(),
|
||||
uploadIntentConfig ?? TestConfigurations.UploadIntent());
|
||||
}
|
||||
|
||||
return new RequestPlayListPictureUploadCommandHandler(
|
||||
Database,
|
||||
new UploadIntentValidator(Database, storageService),
|
||||
storageService,
|
||||
NoOpLogger<RequestPlayListPictureUploadCommandHandler>(),
|
||||
TestConfigurations.Storage(),
|
||||
TestConfigurations.PlayList(),
|
||||
uploadIntentConfig ?? TestConfigurations.UploadIntent());
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_UserExists_IssuesUploadUrlAndCreatesIntent()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
await SeedAsync(user);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UserExists_IssuesUploadUrlAndCreatesIntent()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
await SeedAsync(user);
|
||||
var command = new RequestPlayListPictureUploadCommand(user.Id, "webp", "image/webp");
|
||||
|
||||
var command = new RequestPlayListPictureUploadCommand(user.Id, "webp", "image/webp");
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("https://storage.musify.test/presigned-upload", result.Value.UploadUrl);
|
||||
Assert.Single(await Database.UploadIntents.ToListAsync(TestContext.Current.CancellationToken));
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("https://storage.musify.test/presigned-upload", result.Value.UploadUrl);
|
||||
Assert.Single(await Database.UploadIntents.ToListAsync(TestContext.Current.CancellationToken));
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_UserMissing_ReturnsNotFound()
|
||||
{
|
||||
var command = new RequestPlayListPictureUploadCommand(404, "webp", "image/webp");
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UserMissing_ReturnsNotFound()
|
||||
{
|
||||
var command = new RequestPlayListPictureUploadCommand(404, "webp", "image/webp");
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_UserOverByteQuota_ReturnsValidationErrorAndCreatesNoIntent()
|
||||
{
|
||||
var config = TestConfigurations.UploadIntent();
|
||||
config.MaxActiveUploadBytesPerUser = 100;
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UserOverByteQuota_ReturnsValidationErrorAndCreatesNoIntent()
|
||||
{
|
||||
var config = TestConfigurations.UploadIntent();
|
||||
config.MaxActiveUploadBytesPerUser = 100;
|
||||
var user = TestEntities.User();
|
||||
await SeedAsync(user);
|
||||
|
||||
var user = TestEntities.User();
|
||||
await SeedAsync(user);
|
||||
var command = new RequestPlayListPictureUploadCommand(user.Id, "webp", "image/webp", ExpectedSizeBytes: 1_000_000);
|
||||
|
||||
var command = new RequestPlayListPictureUploadCommand(user.Id, "webp", "image/webp", ExpectedSizeBytes: 1_000_000);
|
||||
var result = await CreateHandler(config).Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler(config).Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
|
||||
Assert.Empty(await Database.UploadIntents.ToListAsync(TestContext.Current.CancellationToken));
|
||||
}
|
||||
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
|
||||
Assert.Empty(await Database.UploadIntents.ToListAsync(TestContext.Current.CancellationToken));
|
||||
}
|
||||
}
|
||||
|
||||
+96
-97
@@ -6,131 +6,130 @@ using Musify.Application.Tests.TestSupport;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.PlayLists
|
||||
namespace Musify.Application.Tests.PlayLists;
|
||||
|
||||
public sealed class UpdatePlayListCommandHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class UpdatePlayListCommandHandlerTests : HandlerTestBase
|
||||
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
|
||||
private readonly IStorageService storageService = Substitute.For<IStorageService>();
|
||||
|
||||
private UpdatePlayListCommandHandler CreateHandler() => new(
|
||||
eventBus,
|
||||
Database,
|
||||
new UploadIntentValidator(Database, storageService),
|
||||
NoOpLogger<UpdatePlayListCommandHandler>(),
|
||||
TestConfigurations.Storage(),
|
||||
TestConfigurations.PlayList(),
|
||||
TestConfigurations.UploadIntent());
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_Owner_RenamesPlayList()
|
||||
{
|
||||
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
|
||||
private readonly IStorageService storageService = Substitute.For<IStorageService>();
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id, "Old Name");
|
||||
await SeedAsync(owner, playList);
|
||||
|
||||
private UpdatePlayListCommandHandler CreateHandler() => new(
|
||||
eventBus,
|
||||
Database,
|
||||
new UploadIntentValidator(Database, storageService),
|
||||
NoOpLogger<UpdatePlayListCommandHandler>(),
|
||||
TestConfigurations.Storage(),
|
||||
TestConfigurations.PlayList(),
|
||||
TestConfigurations.UploadIntent());
|
||||
var command = new UpdatePlayListCommand(owner.Id, playList.Id, "New Name", null, null);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_Owner_RenamesPlayList()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id, "Old Name");
|
||||
await SeedAsync(owner, playList);
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
var command = new UpdatePlayListCommand(owner.Id, playList.Id, "New Name", null, null);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("New Name", result.Value.Name);
|
||||
}
|
||||
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task Handle_PlayListMissing_ReturnsNotFound()
|
||||
{
|
||||
var command = new UpdatePlayListCommand(1, Guid.NewGuid(), "New Name", null, null);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("New Name", result.Value.Name);
|
||||
}
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_PlayListMissing_ReturnsNotFound()
|
||||
{
|
||||
var command = new UpdatePlayListCommand(1, Guid.NewGuid(), "New Name", null, null);
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task Handle_NotOwner_ReturnsUnauthorized()
|
||||
{
|
||||
var owner = TestEntities.User(1, "owner");
|
||||
var stranger = TestEntities.User(2, "stranger");
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
await SeedAsync(owner, stranger, playList);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
var command = new UpdatePlayListCommand(stranger.Id, playList.Id, "Hijacked", null, null);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NotOwner_ReturnsUnauthorized()
|
||||
{
|
||||
var owner = TestEntities.User(1, "owner");
|
||||
var stranger = TestEntities.User(2, "stranger");
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
await SeedAsync(owner, stranger, playList);
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
var command = new UpdatePlayListCommand(stranger.Id, playList.Id, "Hijacked", null, null);
|
||||
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
|
||||
}
|
||||
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task Handle_NewVisibility_UpdatesIt()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
await SeedAsync(owner, playList);
|
||||
|
||||
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
|
||||
}
|
||||
var command = new UpdatePlayListCommand(owner.Id, playList.Id, null, null, null, Musify.Domain.ValueObjects.PlaylistVisibility.Public);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NewVisibility_UpdatesIt()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
await SeedAsync(owner, playList);
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
var command = new UpdatePlayListCommand(owner.Id, playList.Id, null, null, null, Musify.Domain.ValueObjects.PlaylistVisibility.Public);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(Musify.Domain.ValueObjects.PlaylistVisibility.Public, result.Value.Visibility);
|
||||
}
|
||||
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task Handle_NoVisibilitySpecified_LeavesItUnchanged()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id, visibility: Musify.Domain.ValueObjects.PlaylistVisibility.Public);
|
||||
await SeedAsync(owner, playList);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(Musify.Domain.ValueObjects.PlaylistVisibility.Public, result.Value.Visibility);
|
||||
}
|
||||
var command = new UpdatePlayListCommand(owner.Id, playList.Id, "New Name", null, null);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NoVisibilitySpecified_LeavesItUnchanged()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id, visibility: Musify.Domain.ValueObjects.PlaylistVisibility.Public);
|
||||
await SeedAsync(owner, playList);
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
var command = new UpdatePlayListCommand(owner.Id, playList.Id, "New Name", null, null);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(Musify.Domain.ValueObjects.PlaylistVisibility.Public, result.Value.Visibility);
|
||||
}
|
||||
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task Handle_BlankNameAndDescription_LeavesThemUnchanged()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id, "Original Name", "Original description");
|
||||
await SeedAsync(owner, playList);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(Musify.Domain.ValueObjects.PlaylistVisibility.Public, result.Value.Visibility);
|
||||
}
|
||||
var command = new UpdatePlayListCommand(owner.Id, playList.Id, " ", " ", null);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_BlankNameAndDescription_LeavesThemUnchanged()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id, "Original Name", "Original description");
|
||||
await SeedAsync(owner, playList);
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
var command = new UpdatePlayListCommand(owner.Id, playList.Id, " ", " ", null);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("Original Name", result.Value.Name);
|
||||
Assert.Equal("Original description", result.Value.Description);
|
||||
}
|
||||
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task Handle_PlayListWithoutPictures_ValidPictureIntent_SetsPictureAndPublishesSourceEvent()
|
||||
{
|
||||
storageService
|
||||
.HeadObjectAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new ObjectMetaData("image/webp", 1024));
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("Original Name", result.Value.Name);
|
||||
Assert.Equal("Original description", result.Value.Description);
|
||||
}
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
playList.Pictures = null;
|
||||
var intent = TestEntities.UploadIntent(owner.Id, Musify.Domain.ValueObjects.UploadIntentPurpose.PlayListPicture, objectName: "new-cover.webp");
|
||||
await SeedAsync(owner, playList, intent);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_PlayListWithoutPictures_ValidPictureIntent_SetsPictureAndPublishesSourceEvent()
|
||||
{
|
||||
storageService
|
||||
.HeadObjectAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new ObjectMetaData("image/webp", 1024));
|
||||
var command = new UpdatePlayListCommand(owner.Id, playList.Id, null, null, intent.Id);
|
||||
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
playList.Pictures = null;
|
||||
var intent = TestEntities.UploadIntent(owner.Id, Musify.Domain.ValueObjects.UploadIntentPurpose.PlayListPicture, objectName: "new-cover.webp");
|
||||
await SeedAsync(owner, playList, intent);
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
var command = new UpdatePlayListCommand(owner.Id, playList.Id, null, null, intent.Id);
|
||||
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("new-cover.webp", result.Value.SmallImageKeyName);
|
||||
var stored = await Database.PlayLists.FindAsync([playList.Id], TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(stored);
|
||||
Assert.Equal("new-cover.webp", stored.Pictures?.OriginalName);
|
||||
await eventBus.Received(1).PublishAsync(Arg.Any<Musify.Application.Events.UpdatePlayListPictureSourceEvent>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("new-cover.webp", result.Value.SmallImageKeyName);
|
||||
var stored = await Database.PlayLists.FindAsync([playList.Id], TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(stored);
|
||||
Assert.Equal("new-cover.webp", stored.Pictures?.OriginalName);
|
||||
await eventBus.Received(1).PublishAsync(Arg.Any<Musify.Application.Events.UpdatePlayListPictureSourceEvent>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,94 +6,93 @@ using Musify.Application.Tests.TestSupport;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Services
|
||||
namespace Musify.Application.Tests.Services;
|
||||
|
||||
public sealed class TrackStreamIssuerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class TrackStreamIssuerTests : HandlerTestBase
|
||||
private readonly IStreamTicketService ticketService = Substitute.For<IStreamTicketService>();
|
||||
|
||||
private TrackStreamIssuer CreateIssuer(PlaybackConfiguration? playback = null) => new(
|
||||
Database,
|
||||
ticketService,
|
||||
TestConfigurations.Track(),
|
||||
TestConfigurations.StreamGateway("https://stream.musify.test/"),
|
||||
playback ?? TestConfigurations.Playback());
|
||||
|
||||
[Fact]
|
||||
public async Task IssueAsync_BuildsManifestUrlAndRecordsListeningHistory()
|
||||
{
|
||||
private readonly IStreamTicketService ticketService = Substitute.For<IStreamTicketService>();
|
||||
ticketService.IssueTicket(7, Arg.Any<string>(), Arg.Any<long?>()).Returns(new StreamTicket("signed-token", 120));
|
||||
var owner = TestEntities.User(7);
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(owner, track);
|
||||
|
||||
private TrackStreamIssuer CreateIssuer(PlaybackConfiguration? playback = null) => new(
|
||||
Database,
|
||||
ticketService,
|
||||
TestConfigurations.Track(),
|
||||
TestConfigurations.StreamGateway("https://stream.musify.test/"),
|
||||
playback ?? TestConfigurations.Playback());
|
||||
var response = await CreateIssuer().IssueAsync(track.Id, "folder-42", userId: 7, TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task IssueAsync_BuildsManifestUrlAndRecordsListeningHistory()
|
||||
{
|
||||
ticketService.IssueTicket(7, Arg.Any<string>(), Arg.Any<long?>()).Returns(new StreamTicket("signed-token", 120));
|
||||
var owner = TestEntities.User(7);
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(owner, track);
|
||||
Assert.Equal("signed-token", response.Ticket);
|
||||
Assert.Equal(120, response.ExpiresInSeconds);
|
||||
Assert.Equal(
|
||||
"https://stream.musify.test/media/Tracks/ProcessedAudios/folder-42/audio.m4a",
|
||||
response.ManifestUrl);
|
||||
|
||||
var response = await CreateIssuer().IssueAsync(track.Id, "folder-42", userId: 7, TestContext.Current.CancellationToken);
|
||||
var history = Assert.Single(await Database.ListeningHistories.ToListAsync(TestContext.Current.CancellationToken));
|
||||
Assert.Equal(7, history.UserId);
|
||||
Assert.Equal(track.Id, history.TrackId);
|
||||
}
|
||||
|
||||
Assert.Equal("signed-token", response.Ticket);
|
||||
Assert.Equal(120, response.ExpiresInSeconds);
|
||||
Assert.Equal(
|
||||
"https://stream.musify.test/media/Tracks/ProcessedAudios/folder-42/audio.m4a",
|
||||
response.ManifestUrl);
|
||||
[Fact]
|
||||
public async Task IssueAsync_PassesTheProcessedAudioKeyPrefixToTheTicketService()
|
||||
{
|
||||
ticketService.IssueTicket(Arg.Any<long?>(), Arg.Any<string>(), Arg.Any<long?>()).Returns(new StreamTicket("token", 60));
|
||||
var owner = TestEntities.User(1);
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(owner, track);
|
||||
|
||||
var history = Assert.Single(await Database.ListeningHistories.ToListAsync(TestContext.Current.CancellationToken));
|
||||
Assert.Equal(7, history.UserId);
|
||||
Assert.Equal(track.Id, history.TrackId);
|
||||
}
|
||||
await CreateIssuer().IssueAsync(track.Id, "folder-42", userId: 1, TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task IssueAsync_PassesTheProcessedAudioKeyPrefixToTheTicketService()
|
||||
{
|
||||
ticketService.IssueTicket(Arg.Any<long?>(), Arg.Any<string>(), Arg.Any<long?>()).Returns(new StreamTicket("token", 60));
|
||||
var owner = TestEntities.User(1);
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(owner, track);
|
||||
ticketService.Received(1).IssueTicket(1, "Tracks/ProcessedAudios/folder-42/", null);
|
||||
}
|
||||
|
||||
await CreateIssuer().IssueAsync(track.Id, "folder-42", userId: 1, TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task IssueAsync_AnonymousUser_SkipsListeningHistoryAndNeverThrows()
|
||||
{
|
||||
ticketService.IssueTicket(Arg.Any<long?>(), Arg.Any<string>(), Arg.Any<long?>()).Returns(new StreamTicket("token", 60));
|
||||
var owner = TestEntities.User(1);
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(owner, track);
|
||||
|
||||
ticketService.Received(1).IssueTicket(1, "Tracks/ProcessedAudios/folder-42/", null);
|
||||
}
|
||||
var response = await CreateIssuer(TestConfigurations.Playback(allowAnonymousListening: true))
|
||||
.IssueAsync(track.Id, "folder-42", userId: null, TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task IssueAsync_AnonymousUser_SkipsListeningHistoryAndNeverThrows()
|
||||
{
|
||||
ticketService.IssueTicket(Arg.Any<long?>(), Arg.Any<string>(), Arg.Any<long?>()).Returns(new StreamTicket("token", 60));
|
||||
var owner = TestEntities.User(1);
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(owner, track);
|
||||
Assert.Equal("token", response.Ticket);
|
||||
Assert.Empty(await Database.ListeningHistories.ToListAsync(TestContext.Current.CancellationToken));
|
||||
}
|
||||
|
||||
var response = await CreateIssuer(TestConfigurations.Playback(allowAnonymousListening: true))
|
||||
.IssueAsync(track.Id, "folder-42", userId: null, TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task IssueAsync_AnonymousUserWithFragmentConfigured_PassesTheByteCapToTheTicketService()
|
||||
{
|
||||
ticketService.IssueTicket(Arg.Any<long?>(), Arg.Any<string>(), Arg.Any<long?>()).Returns(new StreamTicket("token", 60));
|
||||
var owner = TestEntities.User(1);
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(owner, track);
|
||||
|
||||
Assert.Equal("token", response.Ticket);
|
||||
Assert.Empty(await Database.ListeningHistories.ToListAsync(TestContext.Current.CancellationToken));
|
||||
}
|
||||
var playback = TestConfigurations.Playback(allowAnonymousListening: true, anonymousFragmentSeconds: 30);
|
||||
await CreateIssuer(playback).IssueAsync(track.Id, "folder-42", userId: null, TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task IssueAsync_AnonymousUserWithFragmentConfigured_PassesTheByteCapToTheTicketService()
|
||||
{
|
||||
ticketService.IssueTicket(Arg.Any<long?>(), Arg.Any<string>(), Arg.Any<long?>()).Returns(new StreamTicket("token", 60));
|
||||
var owner = TestEntities.User(1);
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(owner, track);
|
||||
ticketService.Received(1).IssueTicket(null, "Tracks/ProcessedAudios/folder-42/", 30L * playback.EstimatedAudioBytesPerSecond);
|
||||
}
|
||||
|
||||
var playback = TestConfigurations.Playback(allowAnonymousListening: true, anonymousFragmentSeconds: 30);
|
||||
await CreateIssuer(playback).IssueAsync(track.Id, "folder-42", userId: null, TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task IssueAsync_AuthenticatedUser_IgnoresTheAnonymousFragmentCap()
|
||||
{
|
||||
ticketService.IssueTicket(Arg.Any<long?>(), Arg.Any<string>(), Arg.Any<long?>()).Returns(new StreamTicket("token", 60));
|
||||
var owner = TestEntities.User(1);
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(owner, track);
|
||||
|
||||
ticketService.Received(1).IssueTicket(null, "Tracks/ProcessedAudios/folder-42/", 30L * playback.EstimatedAudioBytesPerSecond);
|
||||
}
|
||||
var playback = TestConfigurations.Playback(allowAnonymousListening: true, anonymousFragmentSeconds: 30);
|
||||
await CreateIssuer(playback).IssueAsync(track.Id, "folder-42", userId: 1, TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task IssueAsync_AuthenticatedUser_IgnoresTheAnonymousFragmentCap()
|
||||
{
|
||||
ticketService.IssueTicket(Arg.Any<long?>(), Arg.Any<string>(), Arg.Any<long?>()).Returns(new StreamTicket("token", 60));
|
||||
var owner = TestEntities.User(1);
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(owner, track);
|
||||
|
||||
var playback = TestConfigurations.Playback(allowAnonymousListening: true, anonymousFragmentSeconds: 30);
|
||||
await CreateIssuer(playback).IssueAsync(track.Id, "folder-42", userId: 1, TestContext.Current.CancellationToken);
|
||||
|
||||
ticketService.Received(1).IssueTicket(1, "Tracks/ProcessedAudios/folder-42/", null);
|
||||
}
|
||||
ticketService.Received(1).IssueTicket(1, "Tracks/ProcessedAudios/folder-42/", null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,141 +6,140 @@ using Musify.Domain.ValueObjects;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Services
|
||||
namespace Musify.Application.Tests.Services;
|
||||
|
||||
public sealed class UploadIntentValidatorTests : HandlerTestBase
|
||||
{
|
||||
public sealed class UploadIntentValidatorTests : HandlerTestBase
|
||||
private readonly IStorageService storageService = Substitute.For<IStorageService>();
|
||||
private readonly Domain.Entities.User owner = TestEntities.User();
|
||||
|
||||
private UploadIntentValidator CreateValidator() => new(Database, storageService);
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAndLoadAsync_UploadedObjectWithinLimits_ReturnsTheIntent()
|
||||
{
|
||||
private readonly IStorageService storageService = Substitute.For<IStorageService>();
|
||||
private readonly Domain.Entities.User owner = TestEntities.User();
|
||||
storageService
|
||||
.HeadObjectAsync("bucket", "temp/object.webp", Arg.Any<CancellationToken>())
|
||||
.Returns(new ObjectMetaData("image/webp", 2048));
|
||||
|
||||
private UploadIntentValidator CreateValidator() => new(Database, storageService);
|
||||
var intent = TestEntities.UploadIntent(owner.Id, bucket: "bucket", key: "temp/object.webp");
|
||||
await SeedAsync(owner, intent);
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAndLoadAsync_UploadedObjectWithinLimits_ReturnsTheIntent()
|
||||
{
|
||||
storageService
|
||||
.HeadObjectAsync("bucket", "temp/object.webp", Arg.Any<CancellationToken>())
|
||||
.Returns(new ObjectMetaData("image/webp", 2048));
|
||||
var result = await CreateValidator().ValidateAndLoadAsync(TestConfigurations.UploadIntent(), intent.Id, owner.Id, TestContext.Current.CancellationToken);
|
||||
|
||||
var intent = TestEntities.UploadIntent(owner.Id, bucket: "bucket", key: "temp/object.webp");
|
||||
await SeedAsync(owner, intent);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(intent.Id, result.Value.Id);
|
||||
}
|
||||
|
||||
var result = await CreateValidator().ValidateAndLoadAsync(TestConfigurations.UploadIntent(), intent.Id, owner.Id, TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task ValidateAndLoadAsync_IntentMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateValidator().ValidateAndLoadAsync(TestConfigurations.UploadIntent(), Guid.NewGuid(), owner.Id, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(intent.Id, result.Value.Id);
|
||||
}
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAndLoadAsync_IntentMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateValidator().ValidateAndLoadAsync(TestConfigurations.UploadIntent(), Guid.NewGuid(), owner.Id, TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task ValidateAndLoadAsync_IntentBelongsToAnotherUser_ReturnsNotFound()
|
||||
{
|
||||
var stranger = TestEntities.User(2, "stranger");
|
||||
var intent = TestEntities.UploadIntent(owner.Id);
|
||||
await SeedAsync(owner, stranger, intent);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
var result = await CreateValidator().ValidateAndLoadAsync(TestConfigurations.UploadIntent(), intent.Id, stranger.Id, TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAndLoadAsync_IntentBelongsToAnotherUser_ReturnsNotFound()
|
||||
{
|
||||
var stranger = TestEntities.User(2, "stranger");
|
||||
var intent = TestEntities.UploadIntent(owner.Id);
|
||||
await SeedAsync(owner, stranger, intent);
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
var result = await CreateValidator().ValidateAndLoadAsync(TestConfigurations.UploadIntent(), intent.Id, stranger.Id, TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task ValidateAndLoadAsync_AlreadyConsumed_ReturnsConflict()
|
||||
{
|
||||
var intent = TestEntities.UploadIntent(owner.Id, status: UploadIntentStatus.Consumed);
|
||||
await SeedAsync(owner, intent);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
var result = await CreateValidator().ValidateAndLoadAsync(TestConfigurations.UploadIntent(), intent.Id, owner.Id, TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAndLoadAsync_AlreadyConsumed_ReturnsConflict()
|
||||
{
|
||||
var intent = TestEntities.UploadIntent(owner.Id, status: UploadIntentStatus.Consumed);
|
||||
await SeedAsync(owner, intent);
|
||||
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
|
||||
}
|
||||
|
||||
var result = await CreateValidator().ValidateAndLoadAsync(TestConfigurations.UploadIntent(), intent.Id, owner.Id, TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task ValidateAndLoadAsync_PastExpiry_ReturnsValidationError()
|
||||
{
|
||||
var intent = TestEntities.UploadIntent(owner.Id, expiresAt: DateTime.UtcNow.AddMinutes(-1));
|
||||
await SeedAsync(owner, intent);
|
||||
|
||||
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
|
||||
}
|
||||
var result = await CreateValidator().ValidateAndLoadAsync(TestConfigurations.UploadIntent(), intent.Id, owner.Id, TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAndLoadAsync_PastExpiry_ReturnsValidationError()
|
||||
{
|
||||
var intent = TestEntities.UploadIntent(owner.Id, expiresAt: DateTime.UtcNow.AddMinutes(-1));
|
||||
await SeedAsync(owner, intent);
|
||||
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
|
||||
}
|
||||
|
||||
var result = await CreateValidator().ValidateAndLoadAsync(TestConfigurations.UploadIntent(), intent.Id, owner.Id, TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task ValidateAndLoadAsync_ObjectNeverUploaded_ReturnsNotFound()
|
||||
{
|
||||
storageService
|
||||
.HeadObjectAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns((ObjectMetaData?)null);
|
||||
|
||||
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
|
||||
}
|
||||
var intent = TestEntities.UploadIntent(owner.Id);
|
||||
await SeedAsync(owner, intent);
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAndLoadAsync_ObjectNeverUploaded_ReturnsNotFound()
|
||||
{
|
||||
storageService
|
||||
.HeadObjectAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns((ObjectMetaData?)null);
|
||||
var result = await CreateValidator().ValidateAndLoadAsync(TestConfigurations.UploadIntent(), intent.Id, owner.Id, TestContext.Current.CancellationToken);
|
||||
|
||||
var intent = TestEntities.UploadIntent(owner.Id);
|
||||
await SeedAsync(owner, intent);
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
var result = await CreateValidator().ValidateAndLoadAsync(TestConfigurations.UploadIntent(), intent.Id, owner.Id, TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task ValidateAndLoadAsync_UploadedObjectExceedsMaxSize_ReturnsValidationError()
|
||||
{
|
||||
var config = TestConfigurations.UploadIntent();
|
||||
config.MaxUploadBytes = 1024;
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
storageService
|
||||
.HeadObjectAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new ObjectMetaData("image/webp", 2048));
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAndLoadAsync_UploadedObjectExceedsMaxSize_ReturnsValidationError()
|
||||
{
|
||||
var config = TestConfigurations.UploadIntent();
|
||||
config.MaxUploadBytes = 1024;
|
||||
var intent = TestEntities.UploadIntent(owner.Id);
|
||||
await SeedAsync(owner, intent);
|
||||
|
||||
storageService
|
||||
.HeadObjectAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new ObjectMetaData("image/webp", 2048));
|
||||
var result = await CreateValidator().ValidateAndLoadAsync(config, intent.Id, owner.Id, TestContext.Current.CancellationToken);
|
||||
|
||||
var intent = TestEntities.UploadIntent(owner.Id);
|
||||
await SeedAsync(owner, intent);
|
||||
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
|
||||
}
|
||||
|
||||
var result = await CreateValidator().ValidateAndLoadAsync(config, intent.Id, owner.Id, TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task CheckQuotaAsync_WithinLimits_ReturnsSuccess()
|
||||
{
|
||||
await SeedAsync(owner);
|
||||
|
||||
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
|
||||
}
|
||||
var result = await CreateValidator().CheckQuotaAsync(TestConfigurations.UploadIntent(), owner.Id, requiredBytes: 1024, requiredIntentCount: 1, TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task CheckQuotaAsync_WithinLimits_ReturnsSuccess()
|
||||
{
|
||||
await SeedAsync(owner);
|
||||
Assert.False(result.IsError);
|
||||
}
|
||||
|
||||
var result = await CreateValidator().CheckQuotaAsync(TestConfigurations.UploadIntent(), owner.Id, requiredBytes: 1024, requiredIntentCount: 1, TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task CheckQuotaAsync_TooManyActiveIntents_ReturnsValidationError()
|
||||
{
|
||||
var config = TestConfigurations.UploadIntent();
|
||||
config.MaxActiveUploadIntentsPerUser = 1;
|
||||
await SeedAsync(owner, TestEntities.UploadIntent(owner.Id));
|
||||
|
||||
Assert.False(result.IsError);
|
||||
}
|
||||
var result = await CreateValidator().CheckQuotaAsync(config, owner.Id, requiredBytes: 1, requiredIntentCount: 1, TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task CheckQuotaAsync_TooManyActiveIntents_ReturnsValidationError()
|
||||
{
|
||||
var config = TestConfigurations.UploadIntent();
|
||||
config.MaxActiveUploadIntentsPerUser = 1;
|
||||
await SeedAsync(owner, TestEntities.UploadIntent(owner.Id));
|
||||
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
|
||||
}
|
||||
|
||||
var result = await CreateValidator().CheckQuotaAsync(config, owner.Id, requiredBytes: 1, requiredIntentCount: 1, TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task CheckQuotaAsync_ExpiredOrConsumedIntentsDoNotCountTowardsTheLimit()
|
||||
{
|
||||
var config = TestConfigurations.UploadIntent();
|
||||
config.MaxActiveUploadIntentsPerUser = 1;
|
||||
await SeedAsync(
|
||||
owner,
|
||||
TestEntities.UploadIntent(owner.Id, status: UploadIntentStatus.Expired),
|
||||
TestEntities.UploadIntent(owner.Id, status: UploadIntentStatus.Consumed));
|
||||
|
||||
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
|
||||
}
|
||||
var result = await CreateValidator().CheckQuotaAsync(config, owner.Id, requiredBytes: 1, requiredIntentCount: 1, TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task CheckQuotaAsync_ExpiredOrConsumedIntentsDoNotCountTowardsTheLimit()
|
||||
{
|
||||
var config = TestConfigurations.UploadIntent();
|
||||
config.MaxActiveUploadIntentsPerUser = 1;
|
||||
await SeedAsync(
|
||||
owner,
|
||||
TestEntities.UploadIntent(owner.Id, status: UploadIntentStatus.Expired),
|
||||
TestEntities.UploadIntent(owner.Id, status: UploadIntentStatus.Consumed));
|
||||
|
||||
var result = await CreateValidator().CheckQuotaAsync(config, owner.Id, requiredBytes: 1, requiredIntentCount: 1, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
}
|
||||
Assert.False(result.IsError);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +1,40 @@
|
||||
using Musify.Application.Shared;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Shared
|
||||
namespace Musify.Application.Tests.Shared;
|
||||
|
||||
public sealed class StorageKeyTests
|
||||
{
|
||||
public sealed class StorageKeyTests
|
||||
[Fact]
|
||||
public void Combine_MultipleSegments_JoinsThemWithForwardSlashes()
|
||||
{
|
||||
[Fact]
|
||||
public void Combine_MultipleSegments_JoinsThemWithForwardSlashes()
|
||||
{
|
||||
var key = StorageKey.Combine("Tracks", "SmallPictures", "cover.webp");
|
||||
var key = StorageKey.Combine("Tracks", "SmallPictures", "cover.webp");
|
||||
|
||||
Assert.Equal("Tracks/SmallPictures/cover.webp", key);
|
||||
}
|
||||
Assert.Equal("Tracks/SmallPictures/cover.webp", key);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Combine_SegmentsWithSurroundingSlashesAndWhitespace_TrimsThem()
|
||||
{
|
||||
var key = StorageKey.Combine(" Tracks/ ", "/SmallPictures/", "cover.webp");
|
||||
[Fact]
|
||||
public void Combine_SegmentsWithSurroundingSlashesAndWhitespace_TrimsThem()
|
||||
{
|
||||
var key = StorageKey.Combine(" Tracks/ ", "/SmallPictures/", "cover.webp");
|
||||
|
||||
Assert.Equal("Tracks/SmallPictures/cover.webp", key);
|
||||
}
|
||||
Assert.Equal("Tracks/SmallPictures/cover.webp", key);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
public void Combine_BlankSegment_IsSkipped(string? blank)
|
||||
{
|
||||
var key = StorageKey.Combine("Tracks", blank!, "cover.webp");
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
public void Combine_BlankSegment_IsSkipped(string? blank)
|
||||
{
|
||||
var key = StorageKey.Combine("Tracks", blank!, "cover.webp");
|
||||
|
||||
Assert.Equal("Tracks/cover.webp", key);
|
||||
}
|
||||
Assert.Equal("Tracks/cover.webp", key);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Combine_NoSegments_ReturnsEmptyString()
|
||||
{
|
||||
Assert.Equal(string.Empty, StorageKey.Combine());
|
||||
}
|
||||
[Fact]
|
||||
public void Combine_NoSegments_ReturnsEmptyString()
|
||||
{
|
||||
Assert.Equal(string.Empty, StorageKey.Combine());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,32 +2,31 @@ using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Application.Tracks.Responses;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Shared
|
||||
namespace Musify.Application.Tests.Shared;
|
||||
|
||||
public sealed class TrackApplicationResponseTests
|
||||
{
|
||||
public sealed class TrackApplicationResponseTests
|
||||
[Fact]
|
||||
public void FromEntity_UsesOwnerNameAsArtist()
|
||||
{
|
||||
[Fact]
|
||||
public void FromEntity_UsesOwnerNameAsArtist()
|
||||
{
|
||||
var owner = TestEntities.User(1, "The Owner");
|
||||
var track = TestEntities.Track(owner);
|
||||
var owner = TestEntities.User(1, "The Owner");
|
||||
var track = TestEntities.Track(owner);
|
||||
|
||||
var response = TrackApplicationResponse.FromEntity(track, listensCount: 3);
|
||||
var response = TrackApplicationResponse.FromEntity(track, listensCount: 3);
|
||||
|
||||
Assert.Equal("The Owner", response.Artist);
|
||||
Assert.Equal(owner.Id, response.OwnerUserId);
|
||||
Assert.Equal(3, response.ListensCount);
|
||||
Assert.False(response.IsExplicit);
|
||||
}
|
||||
Assert.Equal("The Owner", response.Artist);
|
||||
Assert.Equal(owner.Id, response.OwnerUserId);
|
||||
Assert.Equal(3, response.ListensCount);
|
||||
Assert.False(response.IsExplicit);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromEntity_CopiesExplicitFlag()
|
||||
{
|
||||
var track = TestEntities.Track(TestEntities.User(), isExplicit: true);
|
||||
[Fact]
|
||||
public void FromEntity_CopiesExplicitFlag()
|
||||
{
|
||||
var track = TestEntities.Track(TestEntities.User(), isExplicit: true);
|
||||
|
||||
var response = TrackApplicationResponse.FromEntity(track, listensCount: 0);
|
||||
var response = TrackApplicationResponse.FromEntity(track, listensCount: 0);
|
||||
|
||||
Assert.True(response.IsExplicit);
|
||||
}
|
||||
Assert.True(response.IsExplicit);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,26 +2,25 @@ using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.TestSupport
|
||||
namespace Musify.Application.Tests.TestSupport;
|
||||
|
||||
public abstract class HandlerTestBase : IAsyncLifetime
|
||||
{
|
||||
public abstract class HandlerTestBase : IAsyncLifetime
|
||||
private TestDatabase database = null!;
|
||||
|
||||
protected TestDatabase Database => database;
|
||||
|
||||
public async ValueTask InitializeAsync() => database = await TestDatabase.CreateAsync();
|
||||
|
||||
public ValueTask DisposeAsync() => database.DisposeAsync();
|
||||
|
||||
protected static ILogger<T> NoOpLogger<T>() => NullLogger<T>.Instance;
|
||||
|
||||
protected async Task SeedAsync(params object[] entities)
|
||||
{
|
||||
private TestDatabase database = null!;
|
||||
foreach (var entity in entities)
|
||||
Database.Add(entity);
|
||||
|
||||
protected TestDatabase Database => database;
|
||||
|
||||
public async ValueTask InitializeAsync() => database = await TestDatabase.CreateAsync();
|
||||
|
||||
public ValueTask DisposeAsync() => database.DisposeAsync();
|
||||
|
||||
protected static ILogger<T> NoOpLogger<T>() => NullLogger<T>.Instance;
|
||||
|
||||
protected async Task SeedAsync(params object[] entities)
|
||||
{
|
||||
foreach (var entity in entities)
|
||||
Database.Add(entity);
|
||||
|
||||
await Database.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
await Database.SaveChangesAsync(CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,25 @@
|
||||
using Musify.Application.Configuration;
|
||||
|
||||
namespace Musify.Application.Tests.TestSupport
|
||||
namespace Musify.Application.Tests.TestSupport;
|
||||
|
||||
public static class TestConfigurations
|
||||
{
|
||||
public static class TestConfigurations
|
||||
{
|
||||
public static ApplicationStorageConfiguration Storage(string bucket = "test-bucket") =>
|
||||
new() { Bucket = bucket };
|
||||
public static ApplicationStorageConfiguration Storage(string bucket = "test-bucket") =>
|
||||
new() { Bucket = bucket };
|
||||
|
||||
public static TrackConfiguration Track() => new();
|
||||
public static TrackConfiguration Track() => new();
|
||||
|
||||
public static PlayListConfiguration PlayList() => new();
|
||||
public static PlayListConfiguration PlayList() => new();
|
||||
|
||||
public static AlbumConfiguration Album() => new();
|
||||
public static AlbumConfiguration Album() => new();
|
||||
|
||||
public static MixConfiguration Mix() => new();
|
||||
public static MixConfiguration Mix() => new();
|
||||
|
||||
public static UploadIntentConfiguration UploadIntent() => new();
|
||||
public static UploadIntentConfiguration UploadIntent() => new();
|
||||
|
||||
public static StreamGatewayConfiguration StreamGateway(string publicBaseUrl = "https://stream.musify.test") =>
|
||||
new() { PublicBaseUrl = publicBaseUrl };
|
||||
public static StreamGatewayConfiguration StreamGateway(string publicBaseUrl = "https://stream.musify.test") =>
|
||||
new() { PublicBaseUrl = publicBaseUrl };
|
||||
|
||||
public static PlaybackConfiguration Playback(bool allowAnonymousListening = false, int anonymousFragmentSeconds = 0) =>
|
||||
new() { AllowAnonymousListening = allowAnonymousListening, AnonymousFragmentSeconds = anonymousFragmentSeconds };
|
||||
}
|
||||
public static PlaybackConfiguration Playback(bool allowAnonymousListening = false, int anonymousFragmentSeconds = 0) =>
|
||||
new() { AllowAnonymousListening = allowAnonymousListening, AnonymousFragmentSeconds = anonymousFragmentSeconds };
|
||||
}
|
||||
|
||||
@@ -3,180 +3,179 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Domain.Entities;
|
||||
|
||||
namespace Musify.Application.Tests.TestSupport
|
||||
namespace Musify.Application.Tests.TestSupport;
|
||||
|
||||
public sealed class TestDatabase : DbContext, IDatabase
|
||||
{
|
||||
public sealed class TestDatabase : DbContext, IDatabase
|
||||
private readonly SqliteConnection connection;
|
||||
|
||||
private TestDatabase(SqliteConnection connection, DbContextOptions<TestDatabase> options)
|
||||
: base(options)
|
||||
{
|
||||
private readonly SqliteConnection connection;
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
private TestDatabase(SqliteConnection connection, DbContextOptions<TestDatabase> options)
|
||||
: base(options)
|
||||
public static async Task<TestDatabase> CreateAsync()
|
||||
{
|
||||
var connection = new SqliteConnection("Filename=:memory:");
|
||||
await connection.OpenAsync();
|
||||
|
||||
var options = new DbContextOptionsBuilder<TestDatabase>()
|
||||
.UseSqlite(connection)
|
||||
.Options;
|
||||
|
||||
var database = new TestDatabase(connection, options);
|
||||
await database.Database.EnsureCreatedAsync();
|
||||
return database;
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
modelBuilder.Entity<Track>().OwnsOne(track => track.Pictures);
|
||||
modelBuilder.Entity<Track>().Navigation(track => track.Pictures).IsRequired();
|
||||
|
||||
modelBuilder.Entity<Track>().OwnsOne(track => track.Audio);
|
||||
modelBuilder.Entity<Track>().Navigation(track => track.Audio).IsRequired();
|
||||
|
||||
modelBuilder.Entity<Track>()
|
||||
.HasOne(track => track.Owner)
|
||||
.WithMany()
|
||||
.HasForeignKey(track => track.OwnerUserId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
modelBuilder.Entity<PlayList>().OwnsOne(playList => playList.Pictures, pictures =>
|
||||
{
|
||||
this.connection = connection;
|
||||
}
|
||||
pictures.Property(p => p.OriginalName).IsRequired(false);
|
||||
pictures.Property(p => p.SmallName).IsRequired(false);
|
||||
pictures.Property(p => p.MediumName).IsRequired(false);
|
||||
pictures.Property(p => p.LargeName).IsRequired(false);
|
||||
});
|
||||
modelBuilder.Entity<PlayList>().Navigation(playList => playList.Pictures).IsRequired(false);
|
||||
|
||||
public static async Task<TestDatabase> CreateAsync()
|
||||
{
|
||||
var connection = new SqliteConnection("Filename=:memory:");
|
||||
await connection.OpenAsync();
|
||||
modelBuilder.Entity<Album>().OwnsOne(album => album.Pictures);
|
||||
modelBuilder.Entity<Album>().Navigation(album => album.Pictures).IsRequired(false);
|
||||
|
||||
var options = new DbContextOptionsBuilder<TestDatabase>()
|
||||
.UseSqlite(connection)
|
||||
.Options;
|
||||
modelBuilder.Entity<Album>()
|
||||
.HasOne(album => album.Owner)
|
||||
.WithMany()
|
||||
.HasForeignKey(album => album.OwnerUserId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
var database = new TestDatabase(connection, options);
|
||||
await database.Database.EnsureCreatedAsync();
|
||||
return database;
|
||||
}
|
||||
modelBuilder.Entity<AlbumHasTrack>()
|
||||
.HasOne(albumTrack => albumTrack.Album)
|
||||
.WithMany(album => album.AlbumTracks)
|
||||
.HasForeignKey(albumTrack => albumTrack.AlbumId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
modelBuilder.Entity<AlbumHasTrack>()
|
||||
.HasOne(albumTrack => albumTrack.Track)
|
||||
.WithMany(track => track.AlbumTracks)
|
||||
.HasForeignKey(albumTrack => albumTrack.TrackId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
modelBuilder.Entity<Track>().OwnsOne(track => track.Pictures);
|
||||
modelBuilder.Entity<Track>().Navigation(track => track.Pictures).IsRequired();
|
||||
modelBuilder.Entity<AlbumHasTrack>()
|
||||
.HasIndex(albumTrack => new { albumTrack.AlbumId, albumTrack.TrackId })
|
||||
.IsUnique();
|
||||
|
||||
modelBuilder.Entity<Track>().OwnsOne(track => track.Audio);
|
||||
modelBuilder.Entity<Track>().Navigation(track => track.Audio).IsRequired();
|
||||
modelBuilder.Entity<Mix>()
|
||||
.HasIndex(mix => new { mix.UserId, mix.Position });
|
||||
|
||||
modelBuilder.Entity<Track>()
|
||||
.HasOne(track => track.Owner)
|
||||
.WithMany()
|
||||
.HasForeignKey(track => track.OwnerUserId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
modelBuilder.Entity<MixItem>()
|
||||
.HasOne(item => item.Mix)
|
||||
.WithMany(mix => mix.Items)
|
||||
.HasForeignKey(item => item.MixId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
modelBuilder.Entity<PlayList>().OwnsOne(playList => playList.Pictures, pictures =>
|
||||
{
|
||||
pictures.Property(p => p.OriginalName).IsRequired(false);
|
||||
pictures.Property(p => p.SmallName).IsRequired(false);
|
||||
pictures.Property(p => p.MediumName).IsRequired(false);
|
||||
pictures.Property(p => p.LargeName).IsRequired(false);
|
||||
});
|
||||
modelBuilder.Entity<PlayList>().Navigation(playList => playList.Pictures).IsRequired(false);
|
||||
modelBuilder.Entity<MixItem>()
|
||||
.HasOne(item => item.Track)
|
||||
.WithMany()
|
||||
.HasForeignKey(item => item.TrackId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
modelBuilder.Entity<Album>().OwnsOne(album => album.Pictures);
|
||||
modelBuilder.Entity<Album>().Navigation(album => album.Pictures).IsRequired(false);
|
||||
modelBuilder.Entity<MixItem>()
|
||||
.HasIndex(item => new { item.MixId, item.Position });
|
||||
|
||||
modelBuilder.Entity<Album>()
|
||||
.HasOne(album => album.Owner)
|
||||
.WithMany()
|
||||
.HasForeignKey(album => album.OwnerUserId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
modelBuilder.Entity<TrackLike>()
|
||||
.HasIndex(like => new { like.UserId, like.TrackId })
|
||||
.IsUnique();
|
||||
|
||||
modelBuilder.Entity<AlbumHasTrack>()
|
||||
.HasOne(albumTrack => albumTrack.Album)
|
||||
.WithMany(album => album.AlbumTracks)
|
||||
.HasForeignKey(albumTrack => albumTrack.AlbumId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<TrackTag>()
|
||||
.HasOne(trackTag => trackTag.Track)
|
||||
.WithMany(track => track.Tags)
|
||||
.HasForeignKey(trackTag => trackTag.TrackId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
modelBuilder.Entity<AlbumHasTrack>()
|
||||
.HasOne(albumTrack => albumTrack.Track)
|
||||
.WithMany(track => track.AlbumTracks)
|
||||
.HasForeignKey(albumTrack => albumTrack.TrackId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<TrackTag>()
|
||||
.HasIndex(trackTag => new { trackTag.TrackId, trackTag.Tag })
|
||||
.IsUnique();
|
||||
|
||||
modelBuilder.Entity<AlbumHasTrack>()
|
||||
.HasIndex(albumTrack => new { albumTrack.AlbumId, albumTrack.TrackId })
|
||||
.IsUnique();
|
||||
modelBuilder.Entity<UserFollow>()
|
||||
.HasOne(follow => follow.Follower)
|
||||
.WithMany()
|
||||
.HasForeignKey(follow => follow.FollowerId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
modelBuilder.Entity<Mix>()
|
||||
.HasIndex(mix => new { mix.UserId, mix.Position });
|
||||
modelBuilder.Entity<UserFollow>()
|
||||
.HasOne(follow => follow.Followed)
|
||||
.WithMany()
|
||||
.HasForeignKey(follow => follow.FollowedId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
modelBuilder.Entity<MixItem>()
|
||||
.HasOne(item => item.Mix)
|
||||
.WithMany(mix => mix.Items)
|
||||
.HasForeignKey(item => item.MixId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
modelBuilder.Entity<UserFollow>()
|
||||
.HasIndex(follow => new { follow.FollowerId, follow.FollowedId })
|
||||
.IsUnique();
|
||||
}
|
||||
|
||||
modelBuilder.Entity<MixItem>()
|
||||
.HasOne(item => item.Track)
|
||||
.WithMany()
|
||||
.HasForeignKey(item => item.TrackId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
public DbSet<User> Users => Set<User>();
|
||||
|
||||
modelBuilder.Entity<MixItem>()
|
||||
.HasIndex(item => new { item.MixId, item.Position });
|
||||
public DbSet<PlayList> PlayLists => Set<PlayList>();
|
||||
|
||||
modelBuilder.Entity<TrackLike>()
|
||||
.HasIndex(like => new { like.UserId, like.TrackId })
|
||||
.IsUnique();
|
||||
public DbSet<Album> Albums => Set<Album>();
|
||||
|
||||
modelBuilder.Entity<TrackTag>()
|
||||
.HasOne(trackTag => trackTag.Track)
|
||||
.WithMany(track => track.Tags)
|
||||
.HasForeignKey(trackTag => trackTag.TrackId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
public DbSet<AlbumHasTrack> AlbumHasTracks => Set<AlbumHasTrack>();
|
||||
|
||||
modelBuilder.Entity<TrackTag>()
|
||||
.HasIndex(trackTag => new { trackTag.TrackId, trackTag.Tag })
|
||||
.IsUnique();
|
||||
public DbSet<Track> Tracks => Set<Track>();
|
||||
|
||||
modelBuilder.Entity<UserFollow>()
|
||||
.HasOne(follow => follow.Follower)
|
||||
.WithMany()
|
||||
.HasForeignKey(follow => follow.FollowerId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
public DbSet<UserHasTrack> UserHasTracks => Set<UserHasTrack>();
|
||||
|
||||
modelBuilder.Entity<UserFollow>()
|
||||
.HasOne(follow => follow.Followed)
|
||||
.WithMany()
|
||||
.HasForeignKey(follow => follow.FollowedId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
public DbSet<PlayListHasTrack> PlayListHasTracks => Set<PlayListHasTrack>();
|
||||
|
||||
modelBuilder.Entity<UserFollow>()
|
||||
.HasIndex(follow => new { follow.FollowerId, follow.FollowedId })
|
||||
.IsUnique();
|
||||
}
|
||||
public DbSet<Mix> Mixes => Set<Mix>();
|
||||
|
||||
public DbSet<User> Users => Set<User>();
|
||||
public DbSet<MixItem> MixItems => Set<MixItem>();
|
||||
|
||||
public DbSet<PlayList> PlayLists => Set<PlayList>();
|
||||
public DbSet<Upload> Uploads => Set<Upload>();
|
||||
|
||||
public DbSet<Album> Albums => Set<Album>();
|
||||
public DbSet<UploadIntent> UploadIntents => Set<UploadIntent>();
|
||||
|
||||
public DbSet<AlbumHasTrack> AlbumHasTracks => Set<AlbumHasTrack>();
|
||||
public DbSet<ListeningHistory> ListeningHistories => Set<ListeningHistory>();
|
||||
|
||||
public DbSet<Track> Tracks => Set<Track>();
|
||||
public DbSet<TrackLike> TrackLikes => Set<TrackLike>();
|
||||
|
||||
public DbSet<UserHasTrack> UserHasTracks => Set<UserHasTrack>();
|
||||
public DbSet<TrackTag> TrackTags => Set<TrackTag>();
|
||||
|
||||
public DbSet<PlayListHasTrack> PlayListHasTracks => Set<PlayListHasTrack>();
|
||||
public DbSet<UserFollow> UserFollows => Set<UserFollow>();
|
||||
|
||||
public DbSet<Mix> Mixes => Set<Mix>();
|
||||
public async Task<IDatabaseTransaction> BeginTransactionAsync(System.Data.IsolationLevel isolationLevel, CancellationToken cancellationToken)
|
||||
{
|
||||
var transaction = await Database.BeginTransactionAsync(isolationLevel, cancellationToken);
|
||||
return new SqliteDatabaseTransaction(transaction);
|
||||
}
|
||||
|
||||
public DbSet<MixItem> MixItems => Set<MixItem>();
|
||||
public override async ValueTask DisposeAsync()
|
||||
{
|
||||
await base.DisposeAsync();
|
||||
await connection.DisposeAsync();
|
||||
}
|
||||
|
||||
public DbSet<Upload> Uploads => Set<Upload>();
|
||||
private sealed class SqliteDatabaseTransaction(Microsoft.EntityFrameworkCore.Storage.IDbContextTransaction inner)
|
||||
: IDatabaseTransaction
|
||||
{
|
||||
public Task CommitAsync(CancellationToken cancellationToken = default) =>
|
||||
inner.CommitAsync(cancellationToken);
|
||||
|
||||
public DbSet<UploadIntent> UploadIntents => Set<UploadIntent>();
|
||||
|
||||
public DbSet<ListeningHistory> ListeningHistories => Set<ListeningHistory>();
|
||||
|
||||
public DbSet<TrackLike> TrackLikes => Set<TrackLike>();
|
||||
|
||||
public DbSet<TrackTag> TrackTags => Set<TrackTag>();
|
||||
|
||||
public DbSet<UserFollow> UserFollows => Set<UserFollow>();
|
||||
|
||||
public async Task<IDatabaseTransaction> BeginTransactionAsync(System.Data.IsolationLevel isolationLevel, CancellationToken cancellationToken)
|
||||
{
|
||||
var transaction = await Database.BeginTransactionAsync(isolationLevel, cancellationToken);
|
||||
return new SqliteDatabaseTransaction(transaction);
|
||||
}
|
||||
|
||||
public override async ValueTask DisposeAsync()
|
||||
{
|
||||
await base.DisposeAsync();
|
||||
await connection.DisposeAsync();
|
||||
}
|
||||
|
||||
private sealed class SqliteDatabaseTransaction(Microsoft.EntityFrameworkCore.Storage.IDbContextTransaction inner)
|
||||
: IDatabaseTransaction
|
||||
{
|
||||
public Task CommitAsync(CancellationToken cancellationToken = default) =>
|
||||
inner.CommitAsync(cancellationToken);
|
||||
|
||||
public ValueTask DisposeAsync() => inner.DisposeAsync();
|
||||
}
|
||||
public ValueTask DisposeAsync() => inner.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,194 +1,193 @@
|
||||
using Musify.Domain.Entities;
|
||||
using Musify.Domain.ValueObjects;
|
||||
|
||||
namespace Musify.Application.Tests.TestSupport
|
||||
namespace Musify.Application.Tests.TestSupport;
|
||||
|
||||
public static class TestEntities
|
||||
{
|
||||
public static class TestEntities
|
||||
public static User User(
|
||||
long id = 1,
|
||||
string name = "test-user",
|
||||
string? firstName = "Test",
|
||||
string? secondName = "User",
|
||||
string? profilePictureUrl = null) =>
|
||||
new()
|
||||
{
|
||||
Id = id,
|
||||
Name = name,
|
||||
NormalizedName = name.ToUpperInvariant(),
|
||||
FirstName = firstName,
|
||||
SecondName = secondName,
|
||||
ProfilePictureUrl = profilePictureUrl
|
||||
};
|
||||
|
||||
public static TrackPictures ProcessedPictures() => new()
|
||||
{
|
||||
public static User User(
|
||||
long id = 1,
|
||||
string name = "test-user",
|
||||
string? firstName = "Test",
|
||||
string? secondName = "User",
|
||||
string? profilePictureUrl = null) =>
|
||||
new()
|
||||
{
|
||||
Id = id,
|
||||
Name = name,
|
||||
NormalizedName = name.ToUpperInvariant(),
|
||||
FirstName = firstName,
|
||||
SecondName = secondName,
|
||||
ProfilePictureUrl = profilePictureUrl
|
||||
};
|
||||
OriginalName = "original.webp",
|
||||
SmallName = "small.webp",
|
||||
MediumName = "medium.webp",
|
||||
LargeName = "large.webp",
|
||||
ProcessingStatus = ProcessingStatus.Completed
|
||||
};
|
||||
|
||||
public static TrackPictures ProcessedPictures() => new()
|
||||
public static TrackPictures PendingPictures() => new()
|
||||
{
|
||||
OriginalName = "original.webp",
|
||||
ProcessingStatus = ProcessingStatus.Pending
|
||||
};
|
||||
|
||||
public static TrackAudio ProcessedAudio(string folderName = "audio-folder") => new()
|
||||
{
|
||||
OriginalName = "original.mp3",
|
||||
FolderName = folderName,
|
||||
TranscodeStatus = ProcessingStatus.Completed
|
||||
};
|
||||
|
||||
public static TrackAudio PendingAudio() => new()
|
||||
{
|
||||
OriginalName = "original.mp3",
|
||||
TranscodeStatus = ProcessingStatus.Pending
|
||||
};
|
||||
|
||||
public static Track Track(
|
||||
User owner,
|
||||
string title = "Test Track",
|
||||
double durationSeconds = 180,
|
||||
TrackPictures? pictures = null,
|
||||
TrackAudio? audio = null,
|
||||
LifeCycleStatus lifeCycleStatus = LifeCycleStatus.Active,
|
||||
IEnumerable<Genre>? tags = null,
|
||||
bool isExplicit = false)
|
||||
{
|
||||
var track = new Track
|
||||
{
|
||||
OriginalName = "original.webp",
|
||||
SmallName = "small.webp",
|
||||
MediumName = "medium.webp",
|
||||
LargeName = "large.webp",
|
||||
ProcessingStatus = ProcessingStatus.Completed
|
||||
Title = title,
|
||||
NormalizedTitle = title.ToUpperInvariant(),
|
||||
DurationSeconds = durationSeconds,
|
||||
LifeCycleStatus = lifeCycleStatus,
|
||||
IsExplicit = isExplicit,
|
||||
OwnerUserId = owner.Id,
|
||||
Owner = owner,
|
||||
Pictures = pictures ?? ProcessedPictures(),
|
||||
Audio = audio ?? ProcessedAudio()
|
||||
};
|
||||
|
||||
public static TrackPictures PendingPictures() => new()
|
||||
{
|
||||
OriginalName = "original.webp",
|
||||
ProcessingStatus = ProcessingStatus.Pending
|
||||
};
|
||||
track.Tags = (tags ?? [Genre.Pop])
|
||||
.Select(tag => new TrackTag { TrackId = track.Id, Tag = tag })
|
||||
.ToList();
|
||||
|
||||
public static TrackAudio ProcessedAudio(string folderName = "audio-folder") => new()
|
||||
{
|
||||
OriginalName = "original.mp3",
|
||||
FolderName = folderName,
|
||||
TranscodeStatus = ProcessingStatus.Completed
|
||||
};
|
||||
|
||||
public static TrackAudio PendingAudio() => new()
|
||||
{
|
||||
OriginalName = "original.mp3",
|
||||
TranscodeStatus = ProcessingStatus.Pending
|
||||
};
|
||||
|
||||
public static Track Track(
|
||||
User owner,
|
||||
string title = "Test Track",
|
||||
double durationSeconds = 180,
|
||||
TrackPictures? pictures = null,
|
||||
TrackAudio? audio = null,
|
||||
LifeCycleStatus lifeCycleStatus = LifeCycleStatus.Active,
|
||||
IEnumerable<Genre>? tags = null,
|
||||
bool isExplicit = false)
|
||||
{
|
||||
var track = new Track
|
||||
{
|
||||
Title = title,
|
||||
NormalizedTitle = title.ToUpperInvariant(),
|
||||
DurationSeconds = durationSeconds,
|
||||
LifeCycleStatus = lifeCycleStatus,
|
||||
IsExplicit = isExplicit,
|
||||
OwnerUserId = owner.Id,
|
||||
Owner = owner,
|
||||
Pictures = pictures ?? ProcessedPictures(),
|
||||
Audio = audio ?? ProcessedAudio()
|
||||
};
|
||||
|
||||
track.Tags = (tags ?? [Genre.Pop])
|
||||
.Select(tag => new TrackTag { TrackId = track.Id, Tag = tag })
|
||||
.ToList();
|
||||
|
||||
return track;
|
||||
}
|
||||
|
||||
public static PlayListPictures PlayListPictures() => new()
|
||||
{
|
||||
OriginalName = "original.webp",
|
||||
SmallName = "small.webp",
|
||||
MediumName = "medium.webp",
|
||||
LargeName = "large.webp"
|
||||
};
|
||||
|
||||
public static PlayList PlayList(
|
||||
long userId,
|
||||
string name = "Test Playlist",
|
||||
string? description = null,
|
||||
PlayListPictures? pictures = null,
|
||||
LifeCycleStatus lifeCycleStatus = LifeCycleStatus.Active,
|
||||
PlaylistVisibility visibility = PlaylistVisibility.Private) =>
|
||||
new()
|
||||
{
|
||||
UserId = userId,
|
||||
Name = name,
|
||||
NormalizedName = name.ToUpperInvariant(),
|
||||
Description = description,
|
||||
Pictures = pictures ?? PlayListPictures(),
|
||||
LifeCycleStatus = lifeCycleStatus,
|
||||
Visibility = visibility
|
||||
};
|
||||
|
||||
public static AlbumPictures AlbumPictures() => new()
|
||||
{
|
||||
OriginalName = "original.webp",
|
||||
SmallName = "small.webp",
|
||||
MediumName = "medium.webp",
|
||||
LargeName = "large.webp"
|
||||
};
|
||||
|
||||
public static Album Album(
|
||||
long ownerUserId,
|
||||
string title = "Test Album",
|
||||
string? description = null,
|
||||
int? releaseYear = 2024,
|
||||
AlbumPictures? pictures = null) =>
|
||||
new()
|
||||
{
|
||||
Title = title,
|
||||
NormalizedTitle = title.ToUpperInvariant(),
|
||||
Description = description,
|
||||
ReleaseYear = releaseYear,
|
||||
OwnerUserId = ownerUserId,
|
||||
Pictures = pictures ?? AlbumPictures()
|
||||
};
|
||||
|
||||
public static UploadIntent UploadIntent(
|
||||
long userId,
|
||||
UploadIntentPurpose purpose = UploadIntentPurpose.TrackPicture,
|
||||
string bucket = "test-bucket",
|
||||
string key = "temp/object.webp",
|
||||
string objectName = "object.webp",
|
||||
string contentType = "image/webp",
|
||||
long? expectedSizeBytes = 1024,
|
||||
UploadIntentStatus status = UploadIntentStatus.Issued,
|
||||
DateTime? expiresAt = null) =>
|
||||
new()
|
||||
{
|
||||
UserId = userId,
|
||||
Bucket = bucket,
|
||||
Key = key,
|
||||
ObjectName = objectName,
|
||||
ContentType = contentType,
|
||||
ExpectedSizeBytes = expectedSizeBytes,
|
||||
Purpose = purpose,
|
||||
Status = status,
|
||||
ExpiresAt = expiresAt ?? DateTime.UtcNow.AddMinutes(5)
|
||||
};
|
||||
|
||||
public static Mix Mix(
|
||||
long userId,
|
||||
string title = "Test Mix",
|
||||
string? subtitle = null,
|
||||
int position = 0) =>
|
||||
new()
|
||||
{
|
||||
UserId = userId,
|
||||
Title = title,
|
||||
Subtitle = subtitle,
|
||||
Position = position
|
||||
};
|
||||
|
||||
public static MixItem MixItem(
|
||||
Guid mixId,
|
||||
Guid trackId,
|
||||
int position = 0) =>
|
||||
new()
|
||||
{
|
||||
MixId = mixId,
|
||||
Position = position,
|
||||
TrackId = trackId
|
||||
};
|
||||
|
||||
public static ListeningHistory ListeningHistory(
|
||||
long userId,
|
||||
Guid trackId,
|
||||
DateTime? listenedAt = null,
|
||||
double? playedSeconds = null,
|
||||
bool isCounted = true) =>
|
||||
new()
|
||||
{
|
||||
UserId = userId,
|
||||
TrackId = trackId,
|
||||
ListenedAt = listenedAt ?? DateTime.UtcNow,
|
||||
PlayedSeconds = playedSeconds,
|
||||
IsCounted = isCounted
|
||||
};
|
||||
return track;
|
||||
}
|
||||
|
||||
public static PlayListPictures PlayListPictures() => new()
|
||||
{
|
||||
OriginalName = "original.webp",
|
||||
SmallName = "small.webp",
|
||||
MediumName = "medium.webp",
|
||||
LargeName = "large.webp"
|
||||
};
|
||||
|
||||
public static PlayList PlayList(
|
||||
long userId,
|
||||
string name = "Test Playlist",
|
||||
string? description = null,
|
||||
PlayListPictures? pictures = null,
|
||||
LifeCycleStatus lifeCycleStatus = LifeCycleStatus.Active,
|
||||
PlaylistVisibility visibility = PlaylistVisibility.Private) =>
|
||||
new()
|
||||
{
|
||||
UserId = userId,
|
||||
Name = name,
|
||||
NormalizedName = name.ToUpperInvariant(),
|
||||
Description = description,
|
||||
Pictures = pictures ?? PlayListPictures(),
|
||||
LifeCycleStatus = lifeCycleStatus,
|
||||
Visibility = visibility
|
||||
};
|
||||
|
||||
public static AlbumPictures AlbumPictures() => new()
|
||||
{
|
||||
OriginalName = "original.webp",
|
||||
SmallName = "small.webp",
|
||||
MediumName = "medium.webp",
|
||||
LargeName = "large.webp"
|
||||
};
|
||||
|
||||
public static Album Album(
|
||||
long ownerUserId,
|
||||
string title = "Test Album",
|
||||
string? description = null,
|
||||
int? releaseYear = 2024,
|
||||
AlbumPictures? pictures = null) =>
|
||||
new()
|
||||
{
|
||||
Title = title,
|
||||
NormalizedTitle = title.ToUpperInvariant(),
|
||||
Description = description,
|
||||
ReleaseYear = releaseYear,
|
||||
OwnerUserId = ownerUserId,
|
||||
Pictures = pictures ?? AlbumPictures()
|
||||
};
|
||||
|
||||
public static UploadIntent UploadIntent(
|
||||
long userId,
|
||||
UploadIntentPurpose purpose = UploadIntentPurpose.TrackPicture,
|
||||
string bucket = "test-bucket",
|
||||
string key = "temp/object.webp",
|
||||
string objectName = "object.webp",
|
||||
string contentType = "image/webp",
|
||||
long? expectedSizeBytes = 1024,
|
||||
UploadIntentStatus status = UploadIntentStatus.Issued,
|
||||
DateTime? expiresAt = null) =>
|
||||
new()
|
||||
{
|
||||
UserId = userId,
|
||||
Bucket = bucket,
|
||||
Key = key,
|
||||
ObjectName = objectName,
|
||||
ContentType = contentType,
|
||||
ExpectedSizeBytes = expectedSizeBytes,
|
||||
Purpose = purpose,
|
||||
Status = status,
|
||||
ExpiresAt = expiresAt ?? DateTime.UtcNow.AddMinutes(5)
|
||||
};
|
||||
|
||||
public static Mix Mix(
|
||||
long userId,
|
||||
string title = "Test Mix",
|
||||
string? subtitle = null,
|
||||
int position = 0) =>
|
||||
new()
|
||||
{
|
||||
UserId = userId,
|
||||
Title = title,
|
||||
Subtitle = subtitle,
|
||||
Position = position
|
||||
};
|
||||
|
||||
public static MixItem MixItem(
|
||||
Guid mixId,
|
||||
Guid trackId,
|
||||
int position = 0) =>
|
||||
new()
|
||||
{
|
||||
MixId = mixId,
|
||||
Position = position,
|
||||
TrackId = trackId
|
||||
};
|
||||
|
||||
public static ListeningHistory ListeningHistory(
|
||||
long userId,
|
||||
Guid trackId,
|
||||
DateTime? listenedAt = null,
|
||||
double? playedSeconds = null,
|
||||
bool isCounted = true) =>
|
||||
new()
|
||||
{
|
||||
UserId = userId,
|
||||
TrackId = trackId,
|
||||
ListenedAt = listenedAt ?? DateTime.UtcNow,
|
||||
PlayedSeconds = playedSeconds,
|
||||
IsCounted = isCounted
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,143 +8,142 @@ using Musify.Domain.ValueObjects;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Tracks
|
||||
namespace Musify.Application.Tests.Tracks;
|
||||
|
||||
public sealed class CreateTrackCommandHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class CreateTrackCommandHandlerTests : HandlerTestBase
|
||||
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
|
||||
private readonly IStorageService storageService = Substitute.For<IStorageService>();
|
||||
|
||||
private CreateTrackCommandHandler CreateHandler()
|
||||
{
|
||||
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
|
||||
private readonly IStorageService storageService = Substitute.For<IStorageService>();
|
||||
storageService
|
||||
.HeadObjectAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new ObjectMetaData("image/webp", 1024));
|
||||
|
||||
private CreateTrackCommandHandler CreateHandler()
|
||||
{
|
||||
storageService
|
||||
.HeadObjectAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new ObjectMetaData("image/webp", 1024));
|
||||
return new CreateTrackCommandHandler(
|
||||
Database,
|
||||
eventBus,
|
||||
new UploadIntentValidator(Database, storageService),
|
||||
NoOpLogger<CreateTrackCommandHandler>(),
|
||||
TestConfigurations.Storage(),
|
||||
TestConfigurations.Track(),
|
||||
TestConfigurations.UploadIntent());
|
||||
}
|
||||
|
||||
return new CreateTrackCommandHandler(
|
||||
Database,
|
||||
eventBus,
|
||||
new UploadIntentValidator(Database, storageService),
|
||||
NoOpLogger<CreateTrackCommandHandler>(),
|
||||
TestConfigurations.Storage(),
|
||||
TestConfigurations.Track(),
|
||||
TestConfigurations.UploadIntent());
|
||||
}
|
||||
private static (Domain.Entities.UploadIntent Picture, Domain.Entities.UploadIntent Audio) SeedIntents(long userId) =>
|
||||
(TestEntities.UploadIntent(userId, UploadIntentPurpose.TrackPicture, objectName: "cover.webp"),
|
||||
TestEntities.UploadIntent(userId, UploadIntentPurpose.TrackAudio, objectName: "song.mp3"));
|
||||
|
||||
private static (Domain.Entities.UploadIntent Picture, Domain.Entities.UploadIntent Audio) SeedIntents(long userId) =>
|
||||
(TestEntities.UploadIntent(userId, UploadIntentPurpose.TrackPicture, objectName: "cover.webp"),
|
||||
TestEntities.UploadIntent(userId, UploadIntentPurpose.TrackAudio, objectName: "song.mp3"));
|
||||
[Fact]
|
||||
public async Task Handle_ValidIntents_CreatesTrackAndPublishesEvent()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
var (picture, audio) = SeedIntents(user.Id);
|
||||
await SeedAsync(user, picture, audio);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ValidIntents_CreatesTrackAndPublishesEvent()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
var (picture, audio) = SeedIntents(user.Id);
|
||||
await SeedAsync(user, picture, audio);
|
||||
var command = new CreateTrackCommand(user.Id, "My Song", picture.Id, audio.Id, [Genre.Pop, Genre.Rock]);
|
||||
|
||||
var command = new CreateTrackCommand(user.Id, "My Song", picture.Id, audio.Id, [Genre.Pop, Genre.Rock]);
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("My Song", result.Value.Title);
|
||||
var localResponse = Assert.IsType<TrackApplicationResponse>(result.Value);
|
||||
Assert.Equal(user.Id, localResponse.OwnerUserId);
|
||||
Assert.Equal([Genre.Pop, Genre.Rock], localResponse.Tags);
|
||||
await eventBus.Received(1).PublishAsync(Arg.Any<Musify.Application.Events.CreateTrackResourcesEvent>(), Arg.Any<CancellationToken>());
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("My Song", result.Value.Title);
|
||||
var localResponse = Assert.IsType<TrackApplicationResponse>(result.Value);
|
||||
Assert.Equal(user.Id, localResponse.OwnerUserId);
|
||||
Assert.Equal([Genre.Pop, Genre.Rock], localResponse.Tags);
|
||||
await eventBus.Received(1).PublishAsync(Arg.Any<Musify.Application.Events.CreateTrackResourcesEvent>(), Arg.Any<CancellationToken>());
|
||||
var stored = await Database.Tracks.FindAsync([result.Value.Id], TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(stored);
|
||||
Assert.Equal(ProcessingStatus.Pending, stored.Audio.TranscodeStatus);
|
||||
}
|
||||
|
||||
var stored = await Database.Tracks.FindAsync([result.Value.Id], TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(stored);
|
||||
Assert.Equal(ProcessingStatus.Pending, stored.Audio.TranscodeStatus);
|
||||
}
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task Handle_StoresExplicitFlag(bool isExplicit)
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
var (picture, audio) = SeedIntents(user.Id);
|
||||
await SeedAsync(user, picture, audio);
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task Handle_StoresExplicitFlag(bool isExplicit)
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
var (picture, audio) = SeedIntents(user.Id);
|
||||
await SeedAsync(user, picture, audio);
|
||||
var command = new CreateTrackCommand(user.Id, "My Song", picture.Id, audio.Id, [Genre.Pop], isExplicit);
|
||||
|
||||
var command = new CreateTrackCommand(user.Id, "My Song", picture.Id, audio.Id, [Genre.Pop], isExplicit);
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(isExplicit, result.Value.IsExplicit);
|
||||
var stored = await Database.Tracks.FindAsync([result.Value.Id], TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(stored);
|
||||
Assert.Equal(isExplicit, stored.IsExplicit);
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(isExplicit, result.Value.IsExplicit);
|
||||
var stored = await Database.Tracks.FindAsync([result.Value.Id], TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(stored);
|
||||
Assert.Equal(isExplicit, stored.IsExplicit);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_NoTags_ReturnsValidationError()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
var (picture, audio) = SeedIntents(user.Id);
|
||||
await SeedAsync(user, picture, audio);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NoTags_ReturnsValidationError()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
var (picture, audio) = SeedIntents(user.Id);
|
||||
await SeedAsync(user, picture, audio);
|
||||
var command = new CreateTrackCommand(user.Id, "My Song", picture.Id, audio.Id, []);
|
||||
|
||||
var command = new CreateTrackCommand(user.Id, "My Song", picture.Id, audio.Id, []);
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
|
||||
}
|
||||
|
||||
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_IncompatibleTags_ReturnsValidationError()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
var (picture, audio) = SeedIntents(user.Id);
|
||||
await SeedAsync(user, picture, audio);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_IncompatibleTags_ReturnsValidationError()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
var (picture, audio) = SeedIntents(user.Id);
|
||||
await SeedAsync(user, picture, audio);
|
||||
var command = new CreateTrackCommand(user.Id, "My Song", picture.Id, audio.Id, [Genre.Classical, Genre.Metal]);
|
||||
|
||||
var command = new CreateTrackCommand(user.Id, "My Song", picture.Id, audio.Id, [Genre.Classical, Genre.Metal]);
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
|
||||
}
|
||||
|
||||
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_UserMissing_ReturnsNotFound()
|
||||
{
|
||||
var command = new CreateTrackCommand(404, "Orphan Song", Guid.NewGuid(), Guid.NewGuid(), [Genre.Pop]);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UserMissing_ReturnsNotFound()
|
||||
{
|
||||
var command = new CreateTrackCommand(404, "Orphan Song", Guid.NewGuid(), Guid.NewGuid(), [Genre.Pop]);
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_PictureIntentBelongsToAnotherUser_ReturnsError()
|
||||
{
|
||||
var user = TestEntities.User(1, "user");
|
||||
var otherUser = TestEntities.User(2, "other");
|
||||
var (picture, audio) = SeedIntents(otherUser.Id);
|
||||
await SeedAsync(user, otherUser, picture, audio);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_PictureIntentBelongsToAnotherUser_ReturnsError()
|
||||
{
|
||||
var user = TestEntities.User(1, "user");
|
||||
var otherUser = TestEntities.User(2, "other");
|
||||
var (picture, audio) = SeedIntents(otherUser.Id);
|
||||
await SeedAsync(user, otherUser, picture, audio);
|
||||
var command = new CreateTrackCommand(user.Id, "My Song", picture.Id, audio.Id, [Genre.Pop]);
|
||||
|
||||
var command = new CreateTrackCommand(user.Id, "My Song", picture.Id, audio.Id, [Genre.Pop]);
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
Assert.True(result.IsError);
|
||||
await eventBus.DidNotReceive().PublishAsync(Arg.Any<Musify.Application.Events.CreateTrackResourcesEvent>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
Assert.True(result.IsError);
|
||||
await eventBus.DidNotReceive().PublishAsync(Arg.Any<Musify.Application.Events.CreateTrackResourcesEvent>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_AudioIntentAlreadyConsumed_ReturnsError()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
var picture = TestEntities.UploadIntent(user.Id, UploadIntentPurpose.TrackPicture, objectName: "cover.webp");
|
||||
var audio = TestEntities.UploadIntent(user.Id, UploadIntentPurpose.TrackAudio, objectName: "song.mp3", status: UploadIntentStatus.Consumed);
|
||||
await SeedAsync(user, picture, audio);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_AudioIntentAlreadyConsumed_ReturnsError()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
var picture = TestEntities.UploadIntent(user.Id, UploadIntentPurpose.TrackPicture, objectName: "cover.webp");
|
||||
var audio = TestEntities.UploadIntent(user.Id, UploadIntentPurpose.TrackAudio, objectName: "song.mp3", status: UploadIntentStatus.Consumed);
|
||||
await SeedAsync(user, picture, audio);
|
||||
var command = new CreateTrackCommand(user.Id, "My Song", picture.Id, audio.Id, [Genre.Pop]);
|
||||
|
||||
var command = new CreateTrackCommand(user.Id, "My Song", picture.Id, audio.Id, [Genre.Pop]);
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
|
||||
}
|
||||
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
|
||||
}
|
||||
}
|
||||
|
||||
+20
-21
@@ -3,30 +3,29 @@ using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Application.Tracks;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Tracks
|
||||
namespace Musify.Application.Tests.Tracks;
|
||||
|
||||
public sealed class DeleteStaleUncountedListensCommandHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class DeleteStaleUncountedListensCommandHandlerTests : HandlerTestBase
|
||||
private DeleteStaleUncountedListensCommandHandler CreateHandler() =>
|
||||
new(Database, NoOpLogger<DeleteStaleUncountedListensCommandHandler>());
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_DeletesOnlyOldUncountedListens()
|
||||
{
|
||||
private DeleteStaleUncountedListensCommandHandler CreateHandler() =>
|
||||
new(Database, NoOpLogger<DeleteStaleUncountedListensCommandHandler>());
|
||||
var owner = TestEntities.User();
|
||||
var track = TestEntities.Track(owner);
|
||||
var oldUncounted = TestEntities.ListeningHistory(owner.Id, track.Id, DateTime.UtcNow.AddDays(-9), playedSeconds: 2, isCounted: false);
|
||||
var oldCounted = TestEntities.ListeningHistory(owner.Id, track.Id, DateTime.UtcNow.AddDays(-9), playedSeconds: 100);
|
||||
var recentUncounted = TestEntities.ListeningHistory(owner.Id, track.Id, DateTime.UtcNow.AddDays(-2), playedSeconds: 2, isCounted: false);
|
||||
await SeedAsync(owner, track, oldUncounted, oldCounted, recentUncounted);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_DeletesOnlyOldUncountedListens()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var track = TestEntities.Track(owner);
|
||||
var oldUncounted = TestEntities.ListeningHistory(owner.Id, track.Id, DateTime.UtcNow.AddDays(-9), playedSeconds: 2, isCounted: false);
|
||||
var oldCounted = TestEntities.ListeningHistory(owner.Id, track.Id, DateTime.UtcNow.AddDays(-9), playedSeconds: 100);
|
||||
var recentUncounted = TestEntities.ListeningHistory(owner.Id, track.Id, DateTime.UtcNow.AddDays(-2), playedSeconds: 2, isCounted: false);
|
||||
await SeedAsync(owner, track, oldUncounted, oldCounted, recentUncounted);
|
||||
var deleted = await CreateHandler().Handle(new DeleteStaleUncountedListensCommand(), TestContext.Current.CancellationToken);
|
||||
|
||||
var deleted = await CreateHandler().Handle(new DeleteStaleUncountedListensCommand(), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(1, deleted);
|
||||
var remaining = await Database.ListeningHistories.Select(l => l.Id).ToListAsync(TestContext.Current.CancellationToken);
|
||||
Assert.DoesNotContain(oldUncounted.Id, remaining);
|
||||
Assert.Contains(oldCounted.Id, remaining);
|
||||
Assert.Contains(recentUncounted.Id, remaining);
|
||||
}
|
||||
Assert.Equal(1, deleted);
|
||||
var remaining = await Database.ListeningHistories.Select(l => l.Id).ToListAsync(TestContext.Current.CancellationToken);
|
||||
Assert.DoesNotContain(oldUncounted.Id, remaining);
|
||||
Assert.Contains(oldCounted.Id, remaining);
|
||||
Assert.Contains(recentUncounted.Id, remaining);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,61 +6,60 @@ using Musify.Domain.ValueObjects;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Tracks
|
||||
namespace Musify.Application.Tests.Tracks;
|
||||
|
||||
public sealed class DeleteTrackCommandHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class DeleteTrackCommandHandlerTests : HandlerTestBase
|
||||
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
|
||||
|
||||
private DeleteTrackCommandHandler CreateHandler() => new(Database, eventBus, NoOpLogger<DeleteTrackCommandHandler>());
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_Owner_MarksTrackAsRemovingAndPublishesEvent()
|
||||
{
|
||||
private readonly IEventBus eventBus = Substitute.For<IEventBus>();
|
||||
var owner = TestEntities.User();
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(owner, track);
|
||||
|
||||
private DeleteTrackCommandHandler CreateHandler() => new(Database, eventBus, NoOpLogger<DeleteTrackCommandHandler>());
|
||||
var result = await CreateHandler().Handle(new DeleteTrackCommand(owner.Id, track.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_Owner_MarksTrackAsRemovingAndPublishesEvent()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(owner, track);
|
||||
Assert.False(result.IsError);
|
||||
var stored = await Database.Tracks.FindAsync([track.Id], TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(stored);
|
||||
Assert.Equal(LifeCycleStatus.Removing, stored.LifeCycleStatus);
|
||||
await eventBus.Received(1).PublishAsync(Arg.Any<Musify.Application.Events.DeleteTrackEvent>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
var result = await CreateHandler().Handle(new DeleteTrackCommand(owner.Id, track.Id), TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task Handle_TrackMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new DeleteTrackCommand(1, Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
var stored = await Database.Tracks.FindAsync([track.Id], TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(stored);
|
||||
Assert.Equal(LifeCycleStatus.Removing, stored.LifeCycleStatus);
|
||||
await eventBus.Received(1).PublishAsync(Arg.Any<Musify.Application.Events.DeleteTrackEvent>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_TrackMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new DeleteTrackCommand(1, Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task Handle_NotOwner_ReturnsUnauthorized()
|
||||
{
|
||||
var owner = TestEntities.User(1, "owner");
|
||||
var stranger = TestEntities.User(2, "stranger");
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(owner, stranger, track);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
var result = await CreateHandler().Handle(new DeleteTrackCommand(stranger.Id, track.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NotOwner_ReturnsUnauthorized()
|
||||
{
|
||||
var owner = TestEntities.User(1, "owner");
|
||||
var stranger = TestEntities.User(2, "stranger");
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(owner, stranger, track);
|
||||
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
|
||||
}
|
||||
|
||||
var result = await CreateHandler().Handle(new DeleteTrackCommand(stranger.Id, track.Id), TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task Handle_AudioStillProcessing_ReturnsConflict()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var track = TestEntities.Track(owner, audio: TestEntities.PendingAudio());
|
||||
await SeedAsync(owner, track);
|
||||
|
||||
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
|
||||
}
|
||||
var result = await CreateHandler().Handle(new DeleteTrackCommand(owner.Id, track.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_AudioStillProcessing_ReturnsConflict()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var track = TestEntities.Track(owner, audio: TestEntities.PendingAudio());
|
||||
await SeedAsync(owner, track);
|
||||
|
||||
var result = await CreateHandler().Handle(new DeleteTrackCommand(owner.Id, track.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
|
||||
}
|
||||
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
|
||||
}
|
||||
}
|
||||
|
||||
+25
-26
@@ -3,38 +3,37 @@ using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Application.Tracks;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Tracks
|
||||
namespace Musify.Application.Tests.Tracks;
|
||||
|
||||
public sealed class GetLastTrackListenedByUserIdQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class GetLastTrackListenedByUserIdQueryHandlerTests : HandlerTestBase
|
||||
private GetLastTrackListenedByUserIdQueryHandler CreateHandler() => new(Database);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_MultipleListens_ReturnsMostRecentlyListenedTrack()
|
||||
{
|
||||
private GetLastTrackListenedByUserIdQueryHandler CreateHandler() => new(Database);
|
||||
var owner = TestEntities.User();
|
||||
var older = TestEntities.Track(owner, "Older listen");
|
||||
var newer = TestEntities.Track(owner, "Newer listen");
|
||||
await SeedAsync(
|
||||
owner, older, newer,
|
||||
TestEntities.ListeningHistory(owner.Id, older.Id, DateTime.UtcNow.AddMinutes(-10)),
|
||||
TestEntities.ListeningHistory(owner.Id, newer.Id, DateTime.UtcNow));
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_MultipleListens_ReturnsMostRecentlyListenedTrack()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var older = TestEntities.Track(owner, "Older listen");
|
||||
var newer = TestEntities.Track(owner, "Newer listen");
|
||||
await SeedAsync(
|
||||
owner, older, newer,
|
||||
TestEntities.ListeningHistory(owner.Id, older.Id, DateTime.UtcNow.AddMinutes(-10)),
|
||||
TestEntities.ListeningHistory(owner.Id, newer.Id, DateTime.UtcNow));
|
||||
var result = await CreateHandler().Handle(new GetLastTrackListenedByUserIdQuery(owner.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetLastTrackListenedByUserIdQuery(owner.Id), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("Newer listen", result.Value.Title);
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("Newer listen", result.Value.Title);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_NoHistory_ReturnsNotFound()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
await SeedAsync(owner);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NoHistory_ReturnsNotFound()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
await SeedAsync(owner);
|
||||
var result = await CreateHandler().Handle(new GetLastTrackListenedByUserIdQuery(owner.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetLastTrackListenedByUserIdQuery(owner.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
}
|
||||
|
||||
+38
-39
@@ -2,54 +2,53 @@ using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Application.Tracks;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Tracks
|
||||
namespace Musify.Application.Tests.Tracks;
|
||||
|
||||
public sealed class GetListeningHistoryQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class GetListeningHistoryQueryHandlerTests : HandlerTestBase
|
||||
private GetListeningHistoryQueryHandler CreateHandler() => new(Database);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_MultipleListens_ReturnsDistinctTracksMostRecentFirst()
|
||||
{
|
||||
private GetListeningHistoryQueryHandler CreateHandler() => new(Database);
|
||||
var owner = TestEntities.User();
|
||||
var older = TestEntities.Track(owner, "Older listen");
|
||||
var newer = TestEntities.Track(owner, "Newer listen");
|
||||
await SeedAsync(
|
||||
owner, older, newer,
|
||||
TestEntities.ListeningHistory(owner.Id, older.Id, DateTime.UtcNow.AddMinutes(-10)),
|
||||
TestEntities.ListeningHistory(owner.Id, older.Id, DateTime.UtcNow.AddMinutes(-5)),
|
||||
TestEntities.ListeningHistory(owner.Id, newer.Id, DateTime.UtcNow));
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_MultipleListens_ReturnsDistinctTracksMostRecentFirst()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var older = TestEntities.Track(owner, "Older listen");
|
||||
var newer = TestEntities.Track(owner, "Newer listen");
|
||||
await SeedAsync(
|
||||
owner, older, newer,
|
||||
TestEntities.ListeningHistory(owner.Id, older.Id, DateTime.UtcNow.AddMinutes(-10)),
|
||||
TestEntities.ListeningHistory(owner.Id, older.Id, DateTime.UtcNow.AddMinutes(-5)),
|
||||
TestEntities.ListeningHistory(owner.Id, newer.Id, DateTime.UtcNow));
|
||||
var result = await CreateHandler().Handle(new GetListeningHistoryQuery(owner.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetListeningHistoryQuery(owner.Id), TestContext.Current.CancellationToken);
|
||||
Assert.Equal(["Newer listen", "Older listen"], result.Select(track => track.Title));
|
||||
}
|
||||
|
||||
Assert.Equal(["Newer listen", "Older listen"], result.Select(track => track.Title));
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_NoHistory_ReturnsEmpty()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
await SeedAsync(owner);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NoHistory_ReturnsEmpty()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
await SeedAsync(owner);
|
||||
var result = await CreateHandler().Handle(new GetListeningHistoryQuery(owner.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetListeningHistoryQuery(owner.Id), TestContext.Current.CancellationToken);
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
Assert.Empty(result);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_UncountedListens_AreIgnored()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var counted = TestEntities.Track(owner, "Counted");
|
||||
var skipped = TestEntities.Track(owner, "Skipped");
|
||||
await SeedAsync(
|
||||
owner, counted, skipped,
|
||||
TestEntities.ListeningHistory(owner.Id, counted.Id, DateTime.UtcNow.AddMinutes(-5), playedSeconds: 90),
|
||||
TestEntities.ListeningHistory(owner.Id, skipped.Id, DateTime.UtcNow, playedSeconds: 2, isCounted: false));
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UncountedListens_AreIgnored()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var counted = TestEntities.Track(owner, "Counted");
|
||||
var skipped = TestEntities.Track(owner, "Skipped");
|
||||
await SeedAsync(
|
||||
owner, counted, skipped,
|
||||
TestEntities.ListeningHistory(owner.Id, counted.Id, DateTime.UtcNow.AddMinutes(-5), playedSeconds: 90),
|
||||
TestEntities.ListeningHistory(owner.Id, skipped.Id, DateTime.UtcNow, playedSeconds: 2, isCounted: false));
|
||||
var result = await CreateHandler().Handle(new GetListeningHistoryQuery(owner.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetListeningHistoryQuery(owner.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(["Counted"], result.Select(track => track.Title));
|
||||
}
|
||||
Assert.Equal(["Counted"], result.Select(track => track.Title));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,50 +4,49 @@ using Musify.Application.Tracks;
|
||||
using Musify.Application.Tracks.Responses;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Tracks
|
||||
namespace Musify.Application.Tests.Tracks;
|
||||
|
||||
public sealed class GetTrackByIdQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class GetTrackByIdQueryHandlerTests : HandlerTestBase
|
||||
private GetTrackByIdQueryHandler CreateHandler() => new(Database);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_LocalTrack_ReturnsItWithOwnerAsArtist()
|
||||
{
|
||||
private GetTrackByIdQueryHandler CreateHandler() => new(Database);
|
||||
var owner = TestEntities.User(1, "artist-name");
|
||||
var track = TestEntities.Track(owner, "My Song");
|
||||
await SeedAsync(owner, track);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_LocalTrack_ReturnsItWithOwnerAsArtist()
|
||||
{
|
||||
var owner = TestEntities.User(1, "artist-name");
|
||||
var track = TestEntities.Track(owner, "My Song");
|
||||
await SeedAsync(owner, track);
|
||||
var result = await CreateHandler().Handle(new GetTrackByIdQuery(track.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetTrackByIdQuery(track.Id), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("My Song", result.Value.Title);
|
||||
Assert.Equal(owner.Name, result.Value.Artist);
|
||||
var localResponse = Assert.IsType<TrackApplicationResponse>(result.Value);
|
||||
Assert.Equal(owner.Id, localResponse.OwnerUserId);
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("My Song", result.Value.Title);
|
||||
Assert.Equal(owner.Name, result.Value.Artist);
|
||||
var localResponse = Assert.IsType<TrackApplicationResponse>(result.Value);
|
||||
Assert.Equal(owner.Id, localResponse.OwnerUserId);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_TrackMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new GetTrackByIdQuery(Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_TrackMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new GetTrackByIdQuery(Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_ListensCount_OnlyCountsCountedListens()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(
|
||||
owner, track,
|
||||
TestEntities.ListeningHistory(owner.Id, track.Id, playedSeconds: 100),
|
||||
TestEntities.ListeningHistory(owner.Id, track.Id, playedSeconds: 100),
|
||||
TestEntities.ListeningHistory(owner.Id, track.Id, playedSeconds: 1, isCounted: false));
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ListensCount_OnlyCountsCountedListens()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(
|
||||
owner, track,
|
||||
TestEntities.ListeningHistory(owner.Id, track.Id, playedSeconds: 100),
|
||||
TestEntities.ListeningHistory(owner.Id, track.Id, playedSeconds: 100),
|
||||
TestEntities.ListeningHistory(owner.Id, track.Id, playedSeconds: 1, isCounted: false));
|
||||
var result = await CreateHandler().Handle(new GetTrackByIdQuery(track.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetTrackByIdQuery(track.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(2, result.Value.ListensCount);
|
||||
}
|
||||
Assert.Equal(2, result.Value.ListensCount);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,50 +3,49 @@ using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Application.Tracks;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Tracks
|
||||
namespace Musify.Application.Tests.Tracks;
|
||||
|
||||
public sealed class GetTrackCoverQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class GetTrackCoverQueryHandlerTests : HandlerTestBase
|
||||
private GetTrackCoverQueryHandler CreateHandler() =>
|
||||
new(Database, TestConfigurations.Storage("covers-bucket"), TestConfigurations.Track());
|
||||
|
||||
[Theory]
|
||||
[InlineData("small", "small.webp")]
|
||||
[InlineData("medium", "medium.webp")]
|
||||
[InlineData("large", "large.webp")]
|
||||
[InlineData("unknown-size-falls-back-to-medium", "medium.webp")]
|
||||
public async Task Handle_ProcessedTrack_ReturnsRequestedSize(string size, string expectedFileName)
|
||||
{
|
||||
private GetTrackCoverQueryHandler CreateHandler() =>
|
||||
new(Database, TestConfigurations.Storage("covers-bucket"), TestConfigurations.Track());
|
||||
var owner = TestEntities.User();
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(owner, track);
|
||||
|
||||
[Theory]
|
||||
[InlineData("small", "small.webp")]
|
||||
[InlineData("medium", "medium.webp")]
|
||||
[InlineData("large", "large.webp")]
|
||||
[InlineData("unknown-size-falls-back-to-medium", "medium.webp")]
|
||||
public async Task Handle_ProcessedTrack_ReturnsRequestedSize(string size, string expectedFileName)
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(owner, track);
|
||||
var result = await CreateHandler().Handle(new GetTrackCoverQuery(track.Id, size), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetTrackCoverQuery(track.Id, size), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("covers-bucket", result.Value.Bucket);
|
||||
Assert.EndsWith(expectedFileName, result.Value.Key);
|
||||
Assert.Equal("image/webp", result.Value.ContentType);
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("covers-bucket", result.Value.Bucket);
|
||||
Assert.EndsWith(expectedFileName, result.Value.Key);
|
||||
Assert.Equal("image/webp", result.Value.ContentType);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_TrackMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new GetTrackCoverQuery(Guid.NewGuid(), "small"), TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_TrackMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new GetTrackCoverQuery(Guid.NewGuid(), "small"), TestContext.Current.CancellationToken);
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_PictureNotProcessedYet_ReturnsNotFound()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var track = TestEntities.Track(owner, pictures: TestEntities.PendingPictures());
|
||||
await SeedAsync(owner, track);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_PictureNotProcessedYet_ReturnsNotFound()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var track = TestEntities.Track(owner, pictures: TestEntities.PendingPictures());
|
||||
await SeedAsync(owner, track);
|
||||
var result = await CreateHandler().Handle(new GetTrackCoverQuery(track.Id, "small"), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetTrackCoverQuery(track.Id, "small"), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,96 +8,95 @@ using Musify.Application.Tracks;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Tracks
|
||||
namespace Musify.Application.Tests.Tracks;
|
||||
|
||||
public sealed class GetTrackStreamQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class GetTrackStreamQueryHandlerTests : HandlerTestBase
|
||||
private readonly IStreamTicketService ticketService = Substitute.For<IStreamTicketService>();
|
||||
|
||||
private GetTrackStreamQueryHandler CreateHandler(PlaybackConfiguration? playback = null)
|
||||
{
|
||||
private readonly IStreamTicketService ticketService = Substitute.For<IStreamTicketService>();
|
||||
ticketService.IssueTicket(Arg.Any<long?>(), Arg.Any<string>(), Arg.Any<long?>()).Returns(new StreamTicket("ticket-token", 60));
|
||||
|
||||
private GetTrackStreamQueryHandler CreateHandler(PlaybackConfiguration? playback = null)
|
||||
{
|
||||
ticketService.IssueTicket(Arg.Any<long?>(), Arg.Any<string>(), Arg.Any<long?>()).Returns(new StreamTicket("ticket-token", 60));
|
||||
var issuer = new TrackStreamIssuer(
|
||||
Database,
|
||||
ticketService,
|
||||
TestConfigurations.Track(),
|
||||
TestConfigurations.StreamGateway("https://stream.musify.test"),
|
||||
playback ?? TestConfigurations.Playback());
|
||||
|
||||
var issuer = new TrackStreamIssuer(
|
||||
Database,
|
||||
ticketService,
|
||||
TestConfigurations.Track(),
|
||||
TestConfigurations.StreamGateway("https://stream.musify.test"),
|
||||
playback ?? TestConfigurations.Playback());
|
||||
return new GetTrackStreamQueryHandler(Database, issuer, playback ?? TestConfigurations.Playback(), NoOpLogger<GetTrackStreamQueryHandler>());
|
||||
}
|
||||
|
||||
return new GetTrackStreamQueryHandler(Database, issuer, playback ?? TestConfigurations.Playback(), NoOpLogger<GetTrackStreamQueryHandler>());
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_ProcessedTrack_IssuesTicketAndRecordsListen()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(owner, track);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ProcessedTrack_IssuesTicketAndRecordsListen()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(owner, track);
|
||||
var result = await CreateHandler().Handle(new GetTrackStreamQuery(track.Id, owner.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetTrackStreamQuery(track.Id, owner.Id), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("ticket-token", result.Value.Ticket);
|
||||
Assert.Equal(60, result.Value.ExpiresInSeconds);
|
||||
Assert.Contains("audio-folder", result.Value.ManifestUrl);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("ticket-token", result.Value.Ticket);
|
||||
Assert.Equal(60, result.Value.ExpiresInSeconds);
|
||||
Assert.Contains("audio-folder", result.Value.ManifestUrl);
|
||||
var history = Assert.Single(await Database.ListeningHistories.ToListAsync(TestContext.Current.CancellationToken));
|
||||
Assert.Equal(owner.Id, history.UserId);
|
||||
Assert.Equal(track.Id, history.TrackId);
|
||||
Assert.Equal(history.Id, result.Value.ListenId);
|
||||
Assert.False(history.IsCounted);
|
||||
Assert.Null(history.PlayedSeconds);
|
||||
}
|
||||
|
||||
var history = Assert.Single(await Database.ListeningHistories.ToListAsync(TestContext.Current.CancellationToken));
|
||||
Assert.Equal(owner.Id, history.UserId);
|
||||
Assert.Equal(track.Id, history.TrackId);
|
||||
Assert.Equal(history.Id, result.Value.ListenId);
|
||||
Assert.False(history.IsCounted);
|
||||
Assert.Null(history.PlayedSeconds);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_TrackMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new GetTrackStreamQuery(Guid.NewGuid(), 1), TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_TrackMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new GetTrackStreamQuery(Guid.NewGuid(), 1), TestContext.Current.CancellationToken);
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_AudioNotProcessedYet_ReturnsConflict()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var track = TestEntities.Track(owner, audio: TestEntities.PendingAudio());
|
||||
await SeedAsync(owner, track);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_AudioNotProcessedYet_ReturnsConflict()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var track = TestEntities.Track(owner, audio: TestEntities.PendingAudio());
|
||||
await SeedAsync(owner, track);
|
||||
var result = await CreateHandler().Handle(new GetTrackStreamQuery(track.Id, owner.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetTrackStreamQuery(track.Id, owner.Id), TestContext.Current.CancellationToken);
|
||||
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
|
||||
}
|
||||
|
||||
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_AnonymousUser_AnonymousListeningDisabled_ReturnsUnauthorized()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(owner, track);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_AnonymousUser_AnonymousListeningDisabled_ReturnsUnauthorized()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(owner, track);
|
||||
var handler = CreateHandler(TestConfigurations.Playback(allowAnonymousListening: false));
|
||||
|
||||
var handler = CreateHandler(TestConfigurations.Playback(allowAnonymousListening: false));
|
||||
var result = await handler.Handle(new GetTrackStreamQuery(track.Id, UserId: null), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await handler.Handle(new GetTrackStreamQuery(track.Id, UserId: null), TestContext.Current.CancellationToken);
|
||||
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
|
||||
}
|
||||
|
||||
Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_AnonymousUser_AnonymousListeningEnabled_IssuesTicketWithoutRecordingHistory()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(owner, track);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_AnonymousUser_AnonymousListeningEnabled_IssuesTicketWithoutRecordingHistory()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(owner, track);
|
||||
var handler = CreateHandler(TestConfigurations.Playback(allowAnonymousListening: true));
|
||||
|
||||
var handler = CreateHandler(TestConfigurations.Playback(allowAnonymousListening: true));
|
||||
var result = await handler.Handle(new GetTrackStreamQuery(track.Id, UserId: null), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await handler.Handle(new GetTrackStreamQuery(track.Id, UserId: null), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("ticket-token", result.Value.Ticket);
|
||||
Assert.Empty(await Database.ListeningHistories.ToListAsync(TestContext.Current.CancellationToken));
|
||||
}
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("ticket-token", result.Value.Ticket);
|
||||
Assert.Empty(await Database.ListeningHistories.ToListAsync(TestContext.Current.CancellationToken));
|
||||
}
|
||||
}
|
||||
|
||||
+31
-32
@@ -3,44 +3,43 @@ using Musify.Application.Tracks;
|
||||
using Musify.Domain.Entities;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Tracks
|
||||
namespace Musify.Application.Tests.Tracks;
|
||||
|
||||
public sealed class GetTracksByUserIdQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class GetTracksByUserIdQueryHandlerTests : HandlerTestBase
|
||||
private GetTracksByUserIdQueryHandler CreateHandler() => new(Database);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ReturnsOnlyTracksLinkedToThatUser()
|
||||
{
|
||||
private GetTracksByUserIdQueryHandler CreateHandler() => new(Database);
|
||||
var owner = TestEntities.User(1, "owner");
|
||||
var other = TestEntities.User(2, "other");
|
||||
var mine = TestEntities.Track(owner, "Mine");
|
||||
var theirs = TestEntities.Track(other, "Theirs");
|
||||
await SeedAsync(
|
||||
owner, other, mine, theirs,
|
||||
new UserHasTrack { UserId = owner.Id, TrackId = mine.Id },
|
||||
new UserHasTrack { UserId = other.Id, TrackId = theirs.Id });
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ReturnsOnlyTracksLinkedToThatUser()
|
||||
{
|
||||
var owner = TestEntities.User(1, "owner");
|
||||
var other = TestEntities.User(2, "other");
|
||||
var mine = TestEntities.Track(owner, "Mine");
|
||||
var theirs = TestEntities.Track(other, "Theirs");
|
||||
await SeedAsync(
|
||||
owner, other, mine, theirs,
|
||||
new UserHasTrack { UserId = owner.Id, TrackId = mine.Id },
|
||||
new UserHasTrack { UserId = other.Id, TrackId = theirs.Id });
|
||||
var result = await CreateHandler().Handle(new GetTracksByUserIdQuery(owner.Id, Name: null, PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetTracksByUserIdQuery(owner.Id, Name: null, PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
var track = Assert.Single(result.Value.Items);
|
||||
Assert.Equal("Mine", track.Title);
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
var track = Assert.Single(result.Value.Items);
|
||||
Assert.Equal("Mine", track.Title);
|
||||
}
|
||||
[Theory]
|
||||
[InlineData(0, 1)]
|
||||
[InlineData(-5, 1)]
|
||||
public async Task Handle_NonPositivePageNumber_FallsBackToFirstPage(int requestedPageNumber, int expectedPageNumber)
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(owner, track, new UserHasTrack { UserId = owner.Id, TrackId = track.Id });
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, 1)]
|
||||
[InlineData(-5, 1)]
|
||||
public async Task Handle_NonPositivePageNumber_FallsBackToFirstPage(int requestedPageNumber, int expectedPageNumber)
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var track = TestEntities.Track(owner);
|
||||
await SeedAsync(owner, track, new UserHasTrack { UserId = owner.Id, TrackId = track.Id });
|
||||
var result = await CreateHandler().Handle(new GetTracksByUserIdQuery(owner.Id, Name: null, PageNumber: requestedPageNumber, PageSize: 10), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetTracksByUserIdQuery(owner.Id, Name: null, PageNumber: requestedPageNumber, PageSize: 10), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(expectedPageNumber, result.Value.PageNumber);
|
||||
}
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(expectedPageNumber, result.Value.PageNumber);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,64 +3,63 @@ using Musify.Application.Tracks;
|
||||
using Musify.Domain.ValueObjects;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Tracks
|
||||
namespace Musify.Application.Tests.Tracks;
|
||||
|
||||
public sealed class GetTracksQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class GetTracksQueryHandlerTests : HandlerTestBase
|
||||
private GetTracksQueryHandler CreateHandler() => new(Database);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NoNameFilter_ReturnsEveryTrackAUserOwns()
|
||||
{
|
||||
private GetTracksQueryHandler CreateHandler() => new(Database);
|
||||
var owner = TestEntities.User();
|
||||
var trackA = TestEntities.Track(owner, "Alpha");
|
||||
var trackB = TestEntities.Track(owner, "Beta");
|
||||
await SeedAsync(
|
||||
owner, trackA, trackB,
|
||||
new Musify.Domain.Entities.UserHasTrack { UserId = owner.Id, TrackId = trackA.Id },
|
||||
new Musify.Domain.Entities.UserHasTrack { UserId = owner.Id, TrackId = trackB.Id });
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NoNameFilter_ReturnsEveryTrackAUserOwns()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var trackA = TestEntities.Track(owner, "Alpha");
|
||||
var trackB = TestEntities.Track(owner, "Beta");
|
||||
await SeedAsync(
|
||||
owner, trackA, trackB,
|
||||
new Musify.Domain.Entities.UserHasTrack { UserId = owner.Id, TrackId = trackA.Id },
|
||||
new Musify.Domain.Entities.UserHasTrack { UserId = owner.Id, TrackId = trackB.Id });
|
||||
var result = await CreateHandler().Handle(new GetTracksQuery(Name: null, PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetTracksQuery(Name: null, PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(2, result.Value.TotalItemCount);
|
||||
Assert.Equal(2, result.Value.Items.Count);
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(2, result.Value.TotalItemCount);
|
||||
Assert.Equal(2, result.Value.Items.Count);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_NameFilter_ReturnsOnlyMatchingTracks()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var match = TestEntities.Track(owner, "Bohemian Rhapsody");
|
||||
var other = TestEntities.Track(owner, "Imagine");
|
||||
await SeedAsync(
|
||||
owner, match, other,
|
||||
new Musify.Domain.Entities.UserHasTrack { UserId = owner.Id, TrackId = match.Id },
|
||||
new Musify.Domain.Entities.UserHasTrack { UserId = owner.Id, TrackId = other.Id });
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NameFilter_ReturnsOnlyMatchingTracks()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var match = TestEntities.Track(owner, "Bohemian Rhapsody");
|
||||
var other = TestEntities.Track(owner, "Imagine");
|
||||
await SeedAsync(
|
||||
owner, match, other,
|
||||
new Musify.Domain.Entities.UserHasTrack { UserId = owner.Id, TrackId = match.Id },
|
||||
new Musify.Domain.Entities.UserHasTrack { UserId = owner.Id, TrackId = other.Id });
|
||||
var result = await CreateHandler().Handle(new GetTracksQuery(Name: "rhapsody", PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetTracksQuery(Name: "rhapsody", PageNumber: 1, PageSize: 10), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
var item = Assert.Single(result.Value.Items);
|
||||
Assert.Equal("Bohemian Rhapsody", item.Track.Title);
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
var item = Assert.Single(result.Value.Items);
|
||||
Assert.Equal("Bohemian Rhapsody", item.Track.Title);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_GenreFilter_ReturnsOnlyTracksWithThatTag()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var rock = TestEntities.Track(owner, "Loud", tags: [Genre.Rock, Genre.Indie]);
|
||||
var jazz = TestEntities.Track(owner, "Smooth", tags: [Genre.Jazz]);
|
||||
await SeedAsync(
|
||||
owner, rock, jazz,
|
||||
new Musify.Domain.Entities.UserHasTrack { UserId = owner.Id, TrackId = rock.Id },
|
||||
new Musify.Domain.Entities.UserHasTrack { UserId = owner.Id, TrackId = jazz.Id });
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_GenreFilter_ReturnsOnlyTracksWithThatTag()
|
||||
{
|
||||
var owner = TestEntities.User();
|
||||
var rock = TestEntities.Track(owner, "Loud", tags: [Genre.Rock, Genre.Indie]);
|
||||
var jazz = TestEntities.Track(owner, "Smooth", tags: [Genre.Jazz]);
|
||||
await SeedAsync(
|
||||
owner, rock, jazz,
|
||||
new Musify.Domain.Entities.UserHasTrack { UserId = owner.Id, TrackId = rock.Id },
|
||||
new Musify.Domain.Entities.UserHasTrack { UserId = owner.Id, TrackId = jazz.Id });
|
||||
var result = await CreateHandler().Handle(new GetTracksQuery(null, 1, 10, Genre.Indie), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetTracksQuery(null, 1, 10, Genre.Indie), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
var item = Assert.Single(result.Value.Items);
|
||||
Assert.Equal("Loud", item.Track.Title);
|
||||
}
|
||||
Assert.False(result.IsError);
|
||||
var item = Assert.Single(result.Value.Items);
|
||||
Assert.Equal("Loud", item.Track.Title);
|
||||
}
|
||||
}
|
||||
|
||||
+91
-92
@@ -5,123 +5,122 @@ using Musify.Application.Tracks;
|
||||
using Musify.Domain.Entities;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Tracks
|
||||
namespace Musify.Application.Tests.Tracks;
|
||||
|
||||
public sealed class RecordListeningProgressCommandHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class RecordListeningProgressCommandHandlerTests : HandlerTestBase
|
||||
private RecordListeningProgressCommandHandler CreateHandler() => new(Database);
|
||||
|
||||
private async Task<ListeningHistory> ReloadAsync(Guid id) =>
|
||||
await Database.ListeningHistories.AsNoTracking()
|
||||
.SingleAsync(l => l.Id == id, TestContext.Current.CancellationToken);
|
||||
|
||||
private async Task<(User User, ListeningHistory Listen)> SeedListenAsync(
|
||||
double durationSeconds, DateTime listenedAt)
|
||||
{
|
||||
private RecordListeningProgressCommandHandler CreateHandler() => new(Database);
|
||||
var user = TestEntities.User();
|
||||
var track = TestEntities.Track(user, durationSeconds: durationSeconds);
|
||||
var listen = TestEntities.ListeningHistory(user.Id, track.Id, listenedAt, isCounted: false);
|
||||
await SeedAsync(user, track, listen);
|
||||
return (user, listen);
|
||||
}
|
||||
|
||||
private async Task<ListeningHistory> ReloadAsync(Guid id) =>
|
||||
await Database.ListeningHistories.AsNoTracking()
|
||||
.SingleAsync(l => l.Id == id, TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task Handle_BelowThreshold_StoresSecondsWithoutCounting()
|
||||
{
|
||||
var (user, listen) = await SeedListenAsync(200, DateTime.UtcNow.AddSeconds(-60));
|
||||
|
||||
private async Task<(User User, ListeningHistory Listen)> SeedListenAsync(
|
||||
double durationSeconds, DateTime listenedAt)
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
var track = TestEntities.Track(user, durationSeconds: durationSeconds);
|
||||
var listen = TestEntities.ListeningHistory(user.Id, track.Id, listenedAt, isCounted: false);
|
||||
await SeedAsync(user, track, listen);
|
||||
return (user, listen);
|
||||
}
|
||||
var result = await CreateHandler().Handle(
|
||||
new RecordListeningProgressCommand(user.Id, listen.Id, 10), TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_BelowThreshold_StoresSecondsWithoutCounting()
|
||||
{
|
||||
var (user, listen) = await SeedListenAsync(200, DateTime.UtcNow.AddSeconds(-60));
|
||||
Assert.False(result.IsError);
|
||||
var stored = await ReloadAsync(listen.Id);
|
||||
Assert.Equal(10, stored.PlayedSeconds);
|
||||
Assert.False(stored.IsCounted);
|
||||
}
|
||||
|
||||
var result = await CreateHandler().Handle(
|
||||
new RecordListeningProgressCommand(user.Id, listen.Id, 10), TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task Handle_ReachesThirtySeconds_MarksAsCounted()
|
||||
{
|
||||
var (user, listen) = await SeedListenAsync(200, DateTime.UtcNow.AddSeconds(-60));
|
||||
|
||||
Assert.False(result.IsError);
|
||||
var stored = await ReloadAsync(listen.Id);
|
||||
Assert.Equal(10, stored.PlayedSeconds);
|
||||
Assert.False(stored.IsCounted);
|
||||
}
|
||||
await CreateHandler().Handle(
|
||||
new RecordListeningProgressCommand(user.Id, listen.Id, 30), TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ReachesThirtySeconds_MarksAsCounted()
|
||||
{
|
||||
var (user, listen) = await SeedListenAsync(200, DateTime.UtcNow.AddSeconds(-60));
|
||||
Assert.True((await ReloadAsync(listen.Id)).IsCounted);
|
||||
}
|
||||
|
||||
await CreateHandler().Handle(
|
||||
new RecordListeningProgressCommand(user.Id, listen.Id, 30), TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task Handle_ShortTrack_CountsAtHalfDuration()
|
||||
{
|
||||
var (user, listen) = await SeedListenAsync(20, DateTime.UtcNow.AddSeconds(-60));
|
||||
|
||||
Assert.True((await ReloadAsync(listen.Id)).IsCounted);
|
||||
}
|
||||
await CreateHandler().Handle(
|
||||
new RecordListeningProgressCommand(user.Id, listen.Id, 10), TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ShortTrack_CountsAtHalfDuration()
|
||||
{
|
||||
var (user, listen) = await SeedListenAsync(20, DateTime.UtcNow.AddSeconds(-60));
|
||||
Assert.True((await ReloadAsync(listen.Id)).IsCounted);
|
||||
}
|
||||
|
||||
await CreateHandler().Handle(
|
||||
new RecordListeningProgressCommand(user.Id, listen.Id, 10), TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task Handle_LowerValueThanStored_KeepsHighest()
|
||||
{
|
||||
var (user, listen) = await SeedListenAsync(200, DateTime.UtcNow.AddSeconds(-60));
|
||||
var handler = CreateHandler();
|
||||
|
||||
Assert.True((await ReloadAsync(listen.Id)).IsCounted);
|
||||
}
|
||||
await handler.Handle(new RecordListeningProgressCommand(user.Id, listen.Id, 40), TestContext.Current.CancellationToken);
|
||||
await handler.Handle(new RecordListeningProgressCommand(user.Id, listen.Id, 15), TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_LowerValueThanStored_KeepsHighest()
|
||||
{
|
||||
var (user, listen) = await SeedListenAsync(200, DateTime.UtcNow.AddSeconds(-60));
|
||||
var handler = CreateHandler();
|
||||
var stored = await ReloadAsync(listen.Id);
|
||||
Assert.Equal(40, stored.PlayedSeconds);
|
||||
Assert.True(stored.IsCounted);
|
||||
}
|
||||
|
||||
await handler.Handle(new RecordListeningProgressCommand(user.Id, listen.Id, 40), TestContext.Current.CancellationToken);
|
||||
await handler.Handle(new RecordListeningProgressCommand(user.Id, listen.Id, 15), TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task Handle_MoreThanElapsedClock_IsCappedToElapsedTime()
|
||||
{
|
||||
var (user, listen) = await SeedListenAsync(200, DateTime.UtcNow.AddSeconds(-20));
|
||||
|
||||
var stored = await ReloadAsync(listen.Id);
|
||||
Assert.Equal(40, stored.PlayedSeconds);
|
||||
Assert.True(stored.IsCounted);
|
||||
}
|
||||
await CreateHandler().Handle(
|
||||
new RecordListeningProgressCommand(user.Id, listen.Id, 150), TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_MoreThanElapsedClock_IsCappedToElapsedTime()
|
||||
{
|
||||
var (user, listen) = await SeedListenAsync(200, DateTime.UtcNow.AddSeconds(-20));
|
||||
var stored = await ReloadAsync(listen.Id);
|
||||
Assert.InRange(stored.PlayedSeconds!.Value, 24, 27);
|
||||
}
|
||||
|
||||
await CreateHandler().Handle(
|
||||
new RecordListeningProgressCommand(user.Id, listen.Id, 150), TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task Handle_MoreThanTrackDuration_IsCappedToDuration()
|
||||
{
|
||||
var (user, listen) = await SeedListenAsync(50, DateTime.UtcNow.AddMinutes(-30));
|
||||
|
||||
var stored = await ReloadAsync(listen.Id);
|
||||
Assert.InRange(stored.PlayedSeconds!.Value, 24, 27);
|
||||
}
|
||||
await CreateHandler().Handle(
|
||||
new RecordListeningProgressCommand(user.Id, listen.Id, 900), TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_MoreThanTrackDuration_IsCappedToDuration()
|
||||
{
|
||||
var (user, listen) = await SeedListenAsync(50, DateTime.UtcNow.AddMinutes(-30));
|
||||
Assert.Equal(52, (await ReloadAsync(listen.Id)).PlayedSeconds);
|
||||
}
|
||||
|
||||
await CreateHandler().Handle(
|
||||
new RecordListeningProgressCommand(user.Id, listen.Id, 900), TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task Handle_OtherUsersListen_ReturnsNotFound()
|
||||
{
|
||||
var (_, listen) = await SeedListenAsync(200, DateTime.UtcNow.AddSeconds(-60));
|
||||
|
||||
Assert.Equal(52, (await ReloadAsync(listen.Id)).PlayedSeconds);
|
||||
}
|
||||
var result = await CreateHandler().Handle(
|
||||
new RecordListeningProgressCommand(long.MaxValue, listen.Id, 10), TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_OtherUsersListen_ReturnsNotFound()
|
||||
{
|
||||
var (_, listen) = await SeedListenAsync(200, DateTime.UtcNow.AddSeconds(-60));
|
||||
Assert.True(result.IsError);
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
var result = await CreateHandler().Handle(
|
||||
new RecordListeningProgressCommand(long.MaxValue, listen.Id, 10), TestContext.Current.CancellationToken);
|
||||
[Theory]
|
||||
[InlineData(-1)]
|
||||
[InlineData(double.NaN)]
|
||||
[InlineData(double.PositiveInfinity)]
|
||||
public async Task Handle_InvalidSeconds_ReturnsValidationError(double seconds)
|
||||
{
|
||||
var (user, listen) = await SeedListenAsync(200, DateTime.UtcNow.AddSeconds(-60));
|
||||
|
||||
Assert.True(result.IsError);
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
var result = await CreateHandler().Handle(
|
||||
new RecordListeningProgressCommand(user.Id, listen.Id, seconds), TestContext.Current.CancellationToken);
|
||||
|
||||
[Theory]
|
||||
[InlineData(-1)]
|
||||
[InlineData(double.NaN)]
|
||||
[InlineData(double.PositiveInfinity)]
|
||||
public async Task Handle_InvalidSeconds_ReturnsValidationError(double seconds)
|
||||
{
|
||||
var (user, listen) = await SeedListenAsync(200, DateTime.UtcNow.AddSeconds(-60));
|
||||
|
||||
var result = await CreateHandler().Handle(
|
||||
new RecordListeningProgressCommand(user.Id, listen.Id, seconds), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
|
||||
}
|
||||
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
|
||||
}
|
||||
}
|
||||
|
||||
+51
-52
@@ -9,73 +9,72 @@ using Musify.Domain.ValueObjects;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Tracks
|
||||
namespace Musify.Application.Tests.Tracks;
|
||||
|
||||
public sealed class RequestTrackUploadUrlsCommandHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class RequestTrackUploadUrlsCommandHandlerTests : HandlerTestBase
|
||||
private readonly IStorageService storageService = Substitute.For<IStorageService>();
|
||||
|
||||
private RequestTrackUploadUrlsCommandHandler CreateHandler(UploadIntentConfiguration? uploadIntentConfig = null)
|
||||
{
|
||||
private readonly IStorageService storageService = Substitute.For<IStorageService>();
|
||||
storageService
|
||||
.GetUploadUrlAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<TimeSpan>(), Arg.Any<CancellationToken>())
|
||||
.Returns("https://storage.musify.test/presigned-upload");
|
||||
|
||||
private RequestTrackUploadUrlsCommandHandler CreateHandler(UploadIntentConfiguration? uploadIntentConfig = null)
|
||||
{
|
||||
storageService
|
||||
.GetUploadUrlAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<TimeSpan>(), Arg.Any<CancellationToken>())
|
||||
.Returns("https://storage.musify.test/presigned-upload");
|
||||
return new RequestTrackUploadUrlsCommandHandler(
|
||||
Database,
|
||||
new UploadIntentValidator(Database, storageService),
|
||||
storageService,
|
||||
NoOpLogger<RequestTrackUploadUrlsCommandHandler>(),
|
||||
TestConfigurations.Storage(),
|
||||
TestConfigurations.Track(),
|
||||
uploadIntentConfig ?? TestConfigurations.UploadIntent());
|
||||
}
|
||||
|
||||
return new RequestTrackUploadUrlsCommandHandler(
|
||||
Database,
|
||||
new UploadIntentValidator(Database, storageService),
|
||||
storageService,
|
||||
NoOpLogger<RequestTrackUploadUrlsCommandHandler>(),
|
||||
TestConfigurations.Storage(),
|
||||
TestConfigurations.Track(),
|
||||
uploadIntentConfig ?? TestConfigurations.UploadIntent());
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_UserExists_IssuesBothUploadUrlsAndCreatesIntents()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
await SeedAsync(user);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UserExists_IssuesBothUploadUrlsAndCreatesIntents()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
await SeedAsync(user);
|
||||
var command = new RequestTrackUploadUrlsCommand(user.Id, "webp", "image/webp", "mp3", "audio/mpeg");
|
||||
|
||||
var command = new RequestTrackUploadUrlsCommand(user.Id, "webp", "image/webp", "mp3", "audio/mpeg");
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("https://storage.musify.test/presigned-upload", result.Value.PictureUploadUrl);
|
||||
Assert.Equal("https://storage.musify.test/presigned-upload", result.Value.AudioUploadUrl);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("https://storage.musify.test/presigned-upload", result.Value.PictureUploadUrl);
|
||||
Assert.Equal("https://storage.musify.test/presigned-upload", result.Value.AudioUploadUrl);
|
||||
var intents = await Database.UploadIntents.Where(intent => intent.UserId == user.Id).ToListAsync(TestContext.Current.CancellationToken);
|
||||
Assert.Equal(2, intents.Count);
|
||||
Assert.Contains(intents, intent => intent.Purpose == UploadIntentPurpose.TrackPicture);
|
||||
Assert.Contains(intents, intent => intent.Purpose == UploadIntentPurpose.TrackAudio);
|
||||
}
|
||||
|
||||
var intents = await Database.UploadIntents.Where(intent => intent.UserId == user.Id).ToListAsync(TestContext.Current.CancellationToken);
|
||||
Assert.Equal(2, intents.Count);
|
||||
Assert.Contains(intents, intent => intent.Purpose == UploadIntentPurpose.TrackPicture);
|
||||
Assert.Contains(intents, intent => intent.Purpose == UploadIntentPurpose.TrackAudio);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_UserMissing_ReturnsNotFound()
|
||||
{
|
||||
var command = new RequestTrackUploadUrlsCommand(404, "webp", "image/webp", "mp3", "audio/mpeg");
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UserMissing_ReturnsNotFound()
|
||||
{
|
||||
var command = new RequestTrackUploadUrlsCommand(404, "webp", "image/webp", "mp3", "audio/mpeg");
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_UserAtActiveIntentLimit_ReturnsValidationErrorAndCreatesNothing()
|
||||
{
|
||||
var config = TestConfigurations.UploadIntent();
|
||||
config.MaxActiveUploadIntentsPerUser = 1;
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UserAtActiveIntentLimit_ReturnsValidationErrorAndCreatesNothing()
|
||||
{
|
||||
var config = TestConfigurations.UploadIntent();
|
||||
config.MaxActiveUploadIntentsPerUser = 1;
|
||||
var user = TestEntities.User();
|
||||
await SeedAsync(user, TestEntities.UploadIntent(user.Id));
|
||||
|
||||
var user = TestEntities.User();
|
||||
await SeedAsync(user, TestEntities.UploadIntent(user.Id));
|
||||
var command = new RequestTrackUploadUrlsCommand(user.Id, "webp", "image/webp", "mp3", "audio/mpeg");
|
||||
|
||||
var command = new RequestTrackUploadUrlsCommand(user.Id, "webp", "image/webp", "mp3", "audio/mpeg");
|
||||
var result = await CreateHandler(config).Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler(config).Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
|
||||
Assert.Equal(1, await Database.UploadIntents.CountAsync(TestContext.Current.CancellationToken));
|
||||
}
|
||||
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
|
||||
Assert.Equal(1, await Database.UploadIntents.CountAsync(TestContext.Current.CancellationToken));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,39 +3,38 @@ using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Application.Users;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Users
|
||||
namespace Musify.Application.Tests.Users;
|
||||
|
||||
public sealed class CreateUserCommandHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class CreateUserCommandHandlerTests : HandlerTestBase
|
||||
private CreateUserCommandHandler CreateHandler() => new(Database, NoOpLogger<CreateUserCommandHandler>());
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NewId_CreatesUser()
|
||||
{
|
||||
private CreateUserCommandHandler CreateHandler() => new(Database, NoOpLogger<CreateUserCommandHandler>());
|
||||
var command = new CreateUserCommand(1, "Jane Doe", "Jane", "Doe");
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NewId_CreatesUser()
|
||||
{
|
||||
var command = new CreateUserCommand(1, "Jane Doe", "Jane", "Doe");
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("Jane Doe", result.Value.Name);
|
||||
Assert.Equal(1, result.Value.Id);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("Jane Doe", result.Value.Name);
|
||||
Assert.Equal(1, result.Value.Id);
|
||||
var stored = await Database.Users.FindAsync([1L], TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(stored);
|
||||
Assert.Equal("JANE DOE", stored.NormalizedName);
|
||||
}
|
||||
|
||||
var stored = await Database.Users.FindAsync([1L], TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(stored);
|
||||
Assert.Equal("JANE DOE", stored.NormalizedName);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_IdAlreadyExists_ReturnsConflict()
|
||||
{
|
||||
var existing = TestEntities.User(1);
|
||||
await SeedAsync(existing);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_IdAlreadyExists_ReturnsConflict()
|
||||
{
|
||||
var existing = TestEntities.User(1);
|
||||
await SeedAsync(existing);
|
||||
var command = new CreateUserCommand(1, "Someone Else", null, null);
|
||||
|
||||
var command = new CreateUserCommand(1, "Someone Else", null, null);
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
|
||||
}
|
||||
Assert.Equal(ErrorType.Conflict, result.FirstError.Type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,58 +4,57 @@ using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Application.Users;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Users
|
||||
namespace Musify.Application.Tests.Users;
|
||||
|
||||
public sealed class FollowUserCommandHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class FollowUserCommandHandlerTests : HandlerTestBase
|
||||
private FollowUserCommandHandler CreateHandler() => new(Database, NoOpLogger<FollowUserCommandHandler>());
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ValidTarget_CreatesFollow()
|
||||
{
|
||||
private FollowUserCommandHandler CreateHandler() => new(Database, NoOpLogger<FollowUserCommandHandler>());
|
||||
var follower = TestEntities.User(1, "follower");
|
||||
var followed = TestEntities.User(2, "followed");
|
||||
await SeedAsync(follower, followed);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ValidTarget_CreatesFollow()
|
||||
{
|
||||
var follower = TestEntities.User(1, "follower");
|
||||
var followed = TestEntities.User(2, "followed");
|
||||
await SeedAsync(follower, followed);
|
||||
var result = await CreateHandler().Handle(new FollowUserCommand(follower.Id, followed.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new FollowUserCommand(follower.Id, followed.Id), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Single(await Database.UserFollows.ToListAsync(TestContext.Current.CancellationToken));
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Single(await Database.UserFollows.ToListAsync(TestContext.Current.CancellationToken));
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_AlreadyFollowing_IsIdempotent()
|
||||
{
|
||||
var follower = TestEntities.User(1, "follower");
|
||||
var followed = TestEntities.User(2, "followed");
|
||||
await SeedAsync(follower, followed, new Musify.Domain.Entities.UserFollow { FollowerId = follower.Id, FollowedId = followed.Id });
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_AlreadyFollowing_IsIdempotent()
|
||||
{
|
||||
var follower = TestEntities.User(1, "follower");
|
||||
var followed = TestEntities.User(2, "followed");
|
||||
await SeedAsync(follower, followed, new Musify.Domain.Entities.UserFollow { FollowerId = follower.Id, FollowedId = followed.Id });
|
||||
var result = await CreateHandler().Handle(new FollowUserCommand(follower.Id, followed.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new FollowUserCommand(follower.Id, followed.Id), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Single(await Database.UserFollows.ToListAsync(TestContext.Current.CancellationToken));
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Single(await Database.UserFollows.ToListAsync(TestContext.Current.CancellationToken));
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_SelfFollow_ReturnsValidationError()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
await SeedAsync(user);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_SelfFollow_ReturnsValidationError()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
await SeedAsync(user);
|
||||
var result = await CreateHandler().Handle(new FollowUserCommand(user.Id, user.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new FollowUserCommand(user.Id, user.Id), TestContext.Current.CancellationToken);
|
||||
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
|
||||
}
|
||||
|
||||
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_TargetMissing_ReturnsNotFound()
|
||||
{
|
||||
var follower = TestEntities.User();
|
||||
await SeedAsync(follower);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_TargetMissing_ReturnsNotFound()
|
||||
{
|
||||
var follower = TestEntities.User();
|
||||
await SeedAsync(follower);
|
||||
var result = await CreateHandler().Handle(new FollowUserCommand(follower.Id, 404), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new FollowUserCommand(follower.Id, 404), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
}
|
||||
|
||||
+89
-90
@@ -2,117 +2,116 @@ using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Application.Users;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Users
|
||||
namespace Musify.Application.Tests.Users;
|
||||
|
||||
public sealed class GetListeningStatsQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class GetListeningStatsQueryHandlerTests : HandlerTestBase
|
||||
private GetListeningStatsQueryHandler CreateHandler() => new(Database);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NoHistory_ReturnsZeroes()
|
||||
{
|
||||
private GetListeningStatsQueryHandler CreateHandler() => new(Database);
|
||||
var user = TestEntities.User();
|
||||
await SeedAsync(user);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NoHistory_ReturnsZeroes()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
await SeedAsync(user);
|
||||
var result = await CreateHandler().Handle(new GetListeningStatsQuery(user.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetListeningStatsQuery(user.Id), TestContext.Current.CancellationToken);
|
||||
Assert.Equal(0, result.TracksThisWeek);
|
||||
Assert.Equal(0, result.SecondsThisWeek);
|
||||
Assert.Equal(0, result.StreakDays);
|
||||
}
|
||||
|
||||
Assert.Equal(0, result.TracksThisWeek);
|
||||
Assert.Equal(0, result.SecondsThisWeek);
|
||||
Assert.Equal(0, result.StreakDays);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_ListensThisWeek_CountsDistinctTracksAndTotalSeconds()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
var trackA = TestEntities.Track(user, "Track A", durationSeconds: 200);
|
||||
var trackB = TestEntities.Track(user, "Track B", durationSeconds: 100);
|
||||
await SeedAsync(
|
||||
user, trackA, trackB,
|
||||
TestEntities.ListeningHistory(user.Id, trackA.Id, DateTime.UtcNow.AddDays(-1)),
|
||||
TestEntities.ListeningHistory(user.Id, trackA.Id, DateTime.UtcNow.AddHours(-1)),
|
||||
TestEntities.ListeningHistory(user.Id, trackB.Id, DateTime.UtcNow),
|
||||
TestEntities.ListeningHistory(user.Id, trackB.Id, DateTime.UtcNow.AddDays(-10)));
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ListensThisWeek_CountsDistinctTracksAndTotalSeconds()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
var trackA = TestEntities.Track(user, "Track A", durationSeconds: 200);
|
||||
var trackB = TestEntities.Track(user, "Track B", durationSeconds: 100);
|
||||
await SeedAsync(
|
||||
user, trackA, trackB,
|
||||
TestEntities.ListeningHistory(user.Id, trackA.Id, DateTime.UtcNow.AddDays(-1)),
|
||||
TestEntities.ListeningHistory(user.Id, trackA.Id, DateTime.UtcNow.AddHours(-1)),
|
||||
TestEntities.ListeningHistory(user.Id, trackB.Id, DateTime.UtcNow),
|
||||
TestEntities.ListeningHistory(user.Id, trackB.Id, DateTime.UtcNow.AddDays(-10)));
|
||||
var result = await CreateHandler().Handle(new GetListeningStatsQuery(user.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetListeningStatsQuery(user.Id), TestContext.Current.CancellationToken);
|
||||
Assert.Equal(2, result.TracksThisWeek);
|
||||
Assert.Equal(500, result.SecondsThisWeek);
|
||||
}
|
||||
|
||||
Assert.Equal(2, result.TracksThisWeek);
|
||||
Assert.Equal(500, result.SecondsThisWeek);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_PlayedSeconds_SumsPlayedTimeInsteadOfDuration()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
var track = TestEntities.Track(user, durationSeconds: 200);
|
||||
await SeedAsync(
|
||||
user, track,
|
||||
TestEntities.ListeningHistory(user.Id, track.Id, playedSeconds: 200),
|
||||
TestEntities.ListeningHistory(user.Id, track.Id, playedSeconds: 12, isCounted: false));
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_PlayedSeconds_SumsPlayedTimeInsteadOfDuration()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
var track = TestEntities.Track(user, durationSeconds: 200);
|
||||
await SeedAsync(
|
||||
user, track,
|
||||
TestEntities.ListeningHistory(user.Id, track.Id, playedSeconds: 200),
|
||||
TestEntities.ListeningHistory(user.Id, track.Id, playedSeconds: 12, isCounted: false));
|
||||
var result = await CreateHandler().Handle(new GetListeningStatsQuery(user.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetListeningStatsQuery(user.Id), TestContext.Current.CancellationToken);
|
||||
Assert.Equal(212, result.SecondsThisWeek);
|
||||
}
|
||||
|
||||
Assert.Equal(212, result.SecondsThisWeek);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_UncountedListens_AreExcludedFromTracksAndStreak()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
var track = TestEntities.Track(user);
|
||||
await SeedAsync(
|
||||
user, track,
|
||||
TestEntities.ListeningHistory(user.Id, track.Id, playedSeconds: 3, isCounted: false));
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UncountedListens_AreExcludedFromTracksAndStreak()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
var track = TestEntities.Track(user);
|
||||
await SeedAsync(
|
||||
user, track,
|
||||
TestEntities.ListeningHistory(user.Id, track.Id, playedSeconds: 3, isCounted: false));
|
||||
var result = await CreateHandler().Handle(new GetListeningStatsQuery(user.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetListeningStatsQuery(user.Id), TestContext.Current.CancellationToken);
|
||||
Assert.Equal(0, result.TracksThisWeek);
|
||||
Assert.Equal(0, result.StreakDays);
|
||||
Assert.Equal(3, result.SecondsThisWeek);
|
||||
}
|
||||
|
||||
Assert.Equal(0, result.TracksThisWeek);
|
||||
Assert.Equal(0, result.StreakDays);
|
||||
Assert.Equal(3, result.SecondsThisWeek);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_ListenedTodayAndYesterday_StreakIsTwo()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
var track = TestEntities.Track(user);
|
||||
await SeedAsync(
|
||||
user, track,
|
||||
TestEntities.ListeningHistory(user.Id, track.Id, DateTime.UtcNow),
|
||||
TestEntities.ListeningHistory(user.Id, track.Id, DateTime.UtcNow.AddDays(-1)));
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ListenedTodayAndYesterday_StreakIsTwo()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
var track = TestEntities.Track(user);
|
||||
await SeedAsync(
|
||||
user, track,
|
||||
TestEntities.ListeningHistory(user.Id, track.Id, DateTime.UtcNow),
|
||||
TestEntities.ListeningHistory(user.Id, track.Id, DateTime.UtcNow.AddDays(-1)));
|
||||
var result = await CreateHandler().Handle(new GetListeningStatsQuery(user.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetListeningStatsQuery(user.Id), TestContext.Current.CancellationToken);
|
||||
Assert.Equal(2, result.StreakDays);
|
||||
}
|
||||
|
||||
Assert.Equal(2, result.StreakDays);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_GapBeforeToday_StopsStreakAtGap()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
var track = TestEntities.Track(user);
|
||||
await SeedAsync(
|
||||
user, track,
|
||||
TestEntities.ListeningHistory(user.Id, track.Id, DateTime.UtcNow),
|
||||
TestEntities.ListeningHistory(user.Id, track.Id, DateTime.UtcNow.AddDays(-2)));
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_GapBeforeToday_StopsStreakAtGap()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
var track = TestEntities.Track(user);
|
||||
await SeedAsync(
|
||||
user, track,
|
||||
TestEntities.ListeningHistory(user.Id, track.Id, DateTime.UtcNow),
|
||||
TestEntities.ListeningHistory(user.Id, track.Id, DateTime.UtcNow.AddDays(-2)));
|
||||
var result = await CreateHandler().Handle(new GetListeningStatsQuery(user.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetListeningStatsQuery(user.Id), TestContext.Current.CancellationToken);
|
||||
Assert.Equal(1, result.StreakDays);
|
||||
}
|
||||
|
||||
Assert.Equal(1, result.StreakDays);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_NoListenToday_StreakIsZero()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
var track = TestEntities.Track(user);
|
||||
await SeedAsync(
|
||||
user, track,
|
||||
TestEntities.ListeningHistory(user.Id, track.Id, DateTime.UtcNow.AddDays(-1)));
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NoListenToday_StreakIsZero()
|
||||
{
|
||||
var user = TestEntities.User();
|
||||
var track = TestEntities.Track(user);
|
||||
await SeedAsync(
|
||||
user, track,
|
||||
TestEntities.ListeningHistory(user.Id, track.Id, DateTime.UtcNow.AddDays(-1)));
|
||||
var result = await CreateHandler().Handle(new GetListeningStatsQuery(user.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetListeningStatsQuery(user.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(0, result.StreakDays);
|
||||
}
|
||||
Assert.Equal(0, result.StreakDays);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,30 +3,29 @@ using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Application.Users;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Users
|
||||
namespace Musify.Application.Tests.Users;
|
||||
|
||||
public sealed class GetUserByIdQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class GetUserByIdQueryHandlerTests : HandlerTestBase
|
||||
private GetUserByIdQueryHandler CreateHandler() => new(Database);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ExistingUser_ReturnsIt()
|
||||
{
|
||||
private GetUserByIdQueryHandler CreateHandler() => new(Database);
|
||||
var user = TestEntities.User(1, "Jane Doe");
|
||||
await SeedAsync(user);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ExistingUser_ReturnsIt()
|
||||
{
|
||||
var user = TestEntities.User(1, "Jane Doe");
|
||||
await SeedAsync(user);
|
||||
var result = await CreateHandler().Handle(new GetUserByIdQuery(1), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetUserByIdQuery(1), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("Jane Doe", result.Value.Name);
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("Jane Doe", result.Value.Name);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_UserMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new GetUserByIdQuery(404), TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UserMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new GetUserByIdQuery(404), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,90 +6,89 @@ using Musify.Application.Users.Responses;
|
||||
using Musify.Domain.Entities;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Users
|
||||
namespace Musify.Application.Tests.Users;
|
||||
|
||||
public sealed class GetUserFollowersQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class GetUserFollowersQueryHandlerTests : HandlerTestBase
|
||||
private GetUserFollowersQueryHandler CreateHandler() => new(Database);
|
||||
|
||||
private async Task<ErrorOr<PaginatedResponse<UserSummaryResponse>>> Send(long? viewerId, long userId) =>
|
||||
await CreateHandler().Handle(new GetUserFollowersQuery(viewerId, userId, 1, 20), TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_OwnProfile_ReturnsFollowers()
|
||||
{
|
||||
private GetUserFollowersQueryHandler CreateHandler() => new(Database);
|
||||
var user = TestEntities.User(1, "user");
|
||||
var follower = TestEntities.User(2, "follower");
|
||||
await SeedAsync(user, follower, new UserFollow { FollowerId = follower.Id, FollowedId = user.Id });
|
||||
|
||||
private async Task<ErrorOr<PaginatedResponse<UserSummaryResponse>>> Send(long? viewerId, long userId) =>
|
||||
await CreateHandler().Handle(new GetUserFollowersQuery(viewerId, userId, 1, 20), TestContext.Current.CancellationToken);
|
||||
var result = await Send(user.Id, user.Id);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_OwnProfile_ReturnsFollowers()
|
||||
{
|
||||
var user = TestEntities.User(1, "user");
|
||||
var follower = TestEntities.User(2, "follower");
|
||||
await SeedAsync(user, follower, new UserFollow { FollowerId = follower.Id, FollowedId = user.Id });
|
||||
Assert.False(result.IsError);
|
||||
var item = Assert.Single(result.Value.Items);
|
||||
Assert.Equal(follower.Id, item.Id);
|
||||
Assert.False(item.IsFollowedByViewer);
|
||||
}
|
||||
|
||||
var result = await Send(user.Id, user.Id);
|
||||
[Fact]
|
||||
public async Task Handle_MutualFollow_ReturnsFollowersWithViewerFollowState()
|
||||
{
|
||||
var viewer = TestEntities.User(1, "viewer");
|
||||
var user = TestEntities.User(2, "user");
|
||||
var other = TestEntities.User(3, "other");
|
||||
await SeedAsync(
|
||||
viewer, user, other,
|
||||
new UserFollow { FollowerId = viewer.Id, FollowedId = user.Id },
|
||||
new UserFollow { FollowerId = user.Id, FollowedId = viewer.Id },
|
||||
new UserFollow { FollowerId = other.Id, FollowedId = user.Id },
|
||||
new UserFollow { FollowerId = viewer.Id, FollowedId = other.Id });
|
||||
|
||||
Assert.False(result.IsError);
|
||||
var item = Assert.Single(result.Value.Items);
|
||||
Assert.Equal(follower.Id, item.Id);
|
||||
Assert.False(item.IsFollowedByViewer);
|
||||
}
|
||||
var result = await Send(viewer.Id, user.Id);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_MutualFollow_ReturnsFollowersWithViewerFollowState()
|
||||
{
|
||||
var viewer = TestEntities.User(1, "viewer");
|
||||
var user = TestEntities.User(2, "user");
|
||||
var other = TestEntities.User(3, "other");
|
||||
await SeedAsync(
|
||||
viewer, user, other,
|
||||
new UserFollow { FollowerId = viewer.Id, FollowedId = user.Id },
|
||||
new UserFollow { FollowerId = user.Id, FollowedId = viewer.Id },
|
||||
new UserFollow { FollowerId = other.Id, FollowedId = user.Id },
|
||||
new UserFollow { FollowerId = viewer.Id, FollowedId = other.Id });
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(2, result.Value.TotalItemCount);
|
||||
Assert.All(result.Value.Items, item => Assert.Equal(item.Id == other.Id, item.IsFollowedByViewer));
|
||||
}
|
||||
|
||||
var result = await Send(viewer.Id, user.Id);
|
||||
[Fact]
|
||||
public async Task Handle_ViewerFollowsButNotFollowedBack_ReturnsForbidden()
|
||||
{
|
||||
var viewer = TestEntities.User(1, "viewer");
|
||||
var user = TestEntities.User(2, "user");
|
||||
await SeedAsync(viewer, user, new UserFollow { FollowerId = viewer.Id, FollowedId = user.Id });
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(2, result.Value.TotalItemCount);
|
||||
Assert.All(result.Value.Items, item => Assert.Equal(item.Id == other.Id, item.IsFollowedByViewer));
|
||||
}
|
||||
var result = await Send(viewer.Id, user.Id);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ViewerFollowsButNotFollowedBack_ReturnsForbidden()
|
||||
{
|
||||
var viewer = TestEntities.User(1, "viewer");
|
||||
var user = TestEntities.User(2, "user");
|
||||
await SeedAsync(viewer, user, new UserFollow { FollowerId = viewer.Id, FollowedId = user.Id });
|
||||
Assert.Equal(ErrorType.Forbidden, result.FirstError.Type);
|
||||
}
|
||||
|
||||
var result = await Send(viewer.Id, user.Id);
|
||||
[Fact]
|
||||
public async Task Handle_FollowedByUserButNotFollowingBack_ReturnsForbidden()
|
||||
{
|
||||
var viewer = TestEntities.User(1, "viewer");
|
||||
var user = TestEntities.User(2, "user");
|
||||
await SeedAsync(viewer, user, new UserFollow { FollowerId = user.Id, FollowedId = viewer.Id });
|
||||
|
||||
Assert.Equal(ErrorType.Forbidden, result.FirstError.Type);
|
||||
}
|
||||
var result = await Send(viewer.Id, user.Id);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_FollowedByUserButNotFollowingBack_ReturnsForbidden()
|
||||
{
|
||||
var viewer = TestEntities.User(1, "viewer");
|
||||
var user = TestEntities.User(2, "user");
|
||||
await SeedAsync(viewer, user, new UserFollow { FollowerId = user.Id, FollowedId = viewer.Id });
|
||||
Assert.Equal(ErrorType.Forbidden, result.FirstError.Type);
|
||||
}
|
||||
|
||||
var result = await Send(viewer.Id, user.Id);
|
||||
[Fact]
|
||||
public async Task Handle_Anonymous_ReturnsForbidden()
|
||||
{
|
||||
await SeedAsync(TestEntities.User(1, "user"));
|
||||
|
||||
Assert.Equal(ErrorType.Forbidden, result.FirstError.Type);
|
||||
}
|
||||
var result = await Send(null, 1);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_Anonymous_ReturnsForbidden()
|
||||
{
|
||||
await SeedAsync(TestEntities.User(1, "user"));
|
||||
Assert.Equal(ErrorType.Forbidden, result.FirstError.Type);
|
||||
}
|
||||
|
||||
var result = await Send(null, 1);
|
||||
[Fact]
|
||||
public async Task Handle_UserMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await Send(1, 404);
|
||||
|
||||
Assert.Equal(ErrorType.Forbidden, result.FirstError.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UserMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await Send(1, 404);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,72 +4,71 @@ using Musify.Application.Users;
|
||||
using Musify.Domain.Entities;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Users
|
||||
namespace Musify.Application.Tests.Users;
|
||||
|
||||
public sealed class GetUserFollowingQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class GetUserFollowingQueryHandlerTests : HandlerTestBase
|
||||
private GetUserFollowingQueryHandler CreateHandler() => new(Database);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_Anonymous_ReturnsFollowedUsers()
|
||||
{
|
||||
private GetUserFollowingQueryHandler CreateHandler() => new(Database);
|
||||
var user = TestEntities.User(1, "user");
|
||||
var followedA = TestEntities.User(2, "followedA");
|
||||
var followedB = TestEntities.User(3, "followedB");
|
||||
await SeedAsync(
|
||||
user, followedA, followedB,
|
||||
new UserFollow { FollowerId = user.Id, FollowedId = followedA.Id },
|
||||
new UserFollow { FollowerId = user.Id, FollowedId = followedB.Id });
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_Anonymous_ReturnsFollowedUsers()
|
||||
{
|
||||
var user = TestEntities.User(1, "user");
|
||||
var followedA = TestEntities.User(2, "followedA");
|
||||
var followedB = TestEntities.User(3, "followedB");
|
||||
await SeedAsync(
|
||||
user, followedA, followedB,
|
||||
new UserFollow { FollowerId = user.Id, FollowedId = followedA.Id },
|
||||
new UserFollow { FollowerId = user.Id, FollowedId = followedB.Id });
|
||||
var result = await CreateHandler().Handle(new GetUserFollowingQuery(null, user.Id, 1, 20), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetUserFollowingQuery(null, user.Id, 1, 20), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(2, result.Value.TotalItemCount);
|
||||
Assert.All(result.Value.Items, item => Assert.False(item.IsFollowedByViewer));
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(2, result.Value.TotalItemCount);
|
||||
Assert.All(result.Value.Items, item => Assert.False(item.IsFollowedByViewer));
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_Viewer_MarksUsersTheViewerFollows()
|
||||
{
|
||||
var viewer = TestEntities.User(1, "viewer");
|
||||
var user = TestEntities.User(2, "user");
|
||||
var followedA = TestEntities.User(3, "followedA");
|
||||
var followedB = TestEntities.User(4, "followedB");
|
||||
await SeedAsync(
|
||||
viewer, user, followedA, followedB,
|
||||
new UserFollow { FollowerId = user.Id, FollowedId = followedA.Id },
|
||||
new UserFollow { FollowerId = user.Id, FollowedId = followedB.Id },
|
||||
new UserFollow { FollowerId = viewer.Id, FollowedId = followedA.Id });
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_Viewer_MarksUsersTheViewerFollows()
|
||||
{
|
||||
var viewer = TestEntities.User(1, "viewer");
|
||||
var user = TestEntities.User(2, "user");
|
||||
var followedA = TestEntities.User(3, "followedA");
|
||||
var followedB = TestEntities.User(4, "followedB");
|
||||
await SeedAsync(
|
||||
viewer, user, followedA, followedB,
|
||||
new UserFollow { FollowerId = user.Id, FollowedId = followedA.Id },
|
||||
new UserFollow { FollowerId = user.Id, FollowedId = followedB.Id },
|
||||
new UserFollow { FollowerId = viewer.Id, FollowedId = followedA.Id });
|
||||
var result = await CreateHandler().Handle(new GetUserFollowingQuery(viewer.Id, user.Id, 1, 20), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetUserFollowingQuery(viewer.Id, user.Id, 1, 20), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
Assert.All(result.Value.Items, item => Assert.Equal(item.Id == followedA.Id, item.IsFollowedByViewer));
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.All(result.Value.Items, item => Assert.Equal(item.Id == followedA.Id, item.IsFollowedByViewer));
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_Pagination_ReturnsRequestedPage()
|
||||
{
|
||||
await SeedAsync(
|
||||
TestEntities.User(1, "user"), TestEntities.User(2, "a"), TestEntities.User(3, "b"), TestEntities.User(4, "c"),
|
||||
new UserFollow { FollowerId = 1, FollowedId = 2 },
|
||||
new UserFollow { FollowerId = 1, FollowedId = 3 },
|
||||
new UserFollow { FollowerId = 1, FollowedId = 4 });
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_Pagination_ReturnsRequestedPage()
|
||||
{
|
||||
await SeedAsync(
|
||||
TestEntities.User(1, "user"), TestEntities.User(2, "a"), TestEntities.User(3, "b"), TestEntities.User(4, "c"),
|
||||
new UserFollow { FollowerId = 1, FollowedId = 2 },
|
||||
new UserFollow { FollowerId = 1, FollowedId = 3 },
|
||||
new UserFollow { FollowerId = 1, FollowedId = 4 });
|
||||
var result = await CreateHandler().Handle(new GetUserFollowingQuery(null, 1, 2, 2), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetUserFollowingQuery(null, 1, 2, 2), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Single(result.Value.Items);
|
||||
Assert.Equal(3, result.Value.TotalItemCount);
|
||||
Assert.False(result.Value.HasNextPage);
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Single(result.Value.Items);
|
||||
Assert.Equal(3, result.Value.TotalItemCount);
|
||||
Assert.False(result.Value.HasNextPage);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_UserMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new GetUserFollowingQuery(null, 404, 1, 20), TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UserMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new GetUserFollowingQuery(null, 404, 1, 20), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,78 +4,77 @@ using Musify.Application.Users;
|
||||
using Musify.Domain.Entities;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Users
|
||||
namespace Musify.Application.Tests.Users;
|
||||
|
||||
public sealed class GetUserProfileQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class GetUserProfileQueryHandlerTests : HandlerTestBase
|
||||
private GetUserProfileQueryHandler CreateHandler() => new(Database);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ExistingUser_ReturnsFollowerAndFollowingCounts()
|
||||
{
|
||||
private GetUserProfileQueryHandler CreateHandler() => new(Database);
|
||||
var user = TestEntities.User(1, "user");
|
||||
var followerA = TestEntities.User(2, "followerA");
|
||||
var followerB = TestEntities.User(3, "followerB");
|
||||
var followed = TestEntities.User(4, "followed");
|
||||
await SeedAsync(
|
||||
user, followerA, followerB, followed,
|
||||
new UserFollow { FollowerId = followerA.Id, FollowedId = user.Id },
|
||||
new UserFollow { FollowerId = followerB.Id, FollowedId = user.Id },
|
||||
new UserFollow { FollowerId = user.Id, FollowedId = followed.Id });
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ExistingUser_ReturnsFollowerAndFollowingCounts()
|
||||
{
|
||||
var user = TestEntities.User(1, "user");
|
||||
var followerA = TestEntities.User(2, "followerA");
|
||||
var followerB = TestEntities.User(3, "followerB");
|
||||
var followed = TestEntities.User(4, "followed");
|
||||
await SeedAsync(
|
||||
user, followerA, followerB, followed,
|
||||
new UserFollow { FollowerId = followerA.Id, FollowedId = user.Id },
|
||||
new UserFollow { FollowerId = followerB.Id, FollowedId = user.Id },
|
||||
new UserFollow { FollowerId = user.Id, FollowedId = followed.Id });
|
||||
var result = await CreateHandler().Handle(new GetUserProfileQuery(user.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetUserProfileQuery(user.Id), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(2, result.Value.FollowersCount);
|
||||
Assert.Equal(1, result.Value.FollowingCount);
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(2, result.Value.FollowersCount);
|
||||
Assert.Equal(1, result.Value.FollowingCount);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_MutualFollow_ViewerCanSeeFollowersAndIsFollowing()
|
||||
{
|
||||
var viewer = TestEntities.User(1, "viewer");
|
||||
var user = TestEntities.User(2, "user");
|
||||
await SeedAsync(
|
||||
viewer, user,
|
||||
new UserFollow { FollowerId = viewer.Id, FollowedId = user.Id },
|
||||
new UserFollow { FollowerId = user.Id, FollowedId = viewer.Id });
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_MutualFollow_ViewerCanSeeFollowersAndIsFollowing()
|
||||
{
|
||||
var viewer = TestEntities.User(1, "viewer");
|
||||
var user = TestEntities.User(2, "user");
|
||||
await SeedAsync(
|
||||
viewer, user,
|
||||
new UserFollow { FollowerId = viewer.Id, FollowedId = user.Id },
|
||||
new UserFollow { FollowerId = user.Id, FollowedId = viewer.Id });
|
||||
var result = await CreateHandler().Handle(new GetUserProfileQuery(user.Id, viewer.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetUserProfileQuery(user.Id, viewer.Id), TestContext.Current.CancellationToken);
|
||||
Assert.True(result.Value.IsFollowing);
|
||||
Assert.True(result.Value.CanViewFollowers);
|
||||
}
|
||||
|
||||
Assert.True(result.Value.IsFollowing);
|
||||
Assert.True(result.Value.CanViewFollowers);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_OneWayFollow_ViewerCannotSeeFollowers()
|
||||
{
|
||||
var viewer = TestEntities.User(1, "viewer");
|
||||
var user = TestEntities.User(2, "user");
|
||||
await SeedAsync(viewer, user, new UserFollow { FollowerId = viewer.Id, FollowedId = user.Id });
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_OneWayFollow_ViewerCannotSeeFollowers()
|
||||
{
|
||||
var viewer = TestEntities.User(1, "viewer");
|
||||
var user = TestEntities.User(2, "user");
|
||||
await SeedAsync(viewer, user, new UserFollow { FollowerId = viewer.Id, FollowedId = user.Id });
|
||||
var result = await CreateHandler().Handle(new GetUserProfileQuery(user.Id, viewer.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetUserProfileQuery(user.Id, viewer.Id), TestContext.Current.CancellationToken);
|
||||
Assert.True(result.Value.IsFollowing);
|
||||
Assert.False(result.Value.CanViewFollowers);
|
||||
}
|
||||
|
||||
Assert.True(result.Value.IsFollowing);
|
||||
Assert.False(result.Value.CanViewFollowers);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_OwnProfile_CanSeeFollowersButIsNotFollowing()
|
||||
{
|
||||
await SeedAsync(TestEntities.User(1, "user"));
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_OwnProfile_CanSeeFollowersButIsNotFollowing()
|
||||
{
|
||||
await SeedAsync(TestEntities.User(1, "user"));
|
||||
var result = await CreateHandler().Handle(new GetUserProfileQuery(1, 1), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetUserProfileQuery(1, 1), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.Value.IsFollowing);
|
||||
Assert.True(result.Value.CanViewFollowers);
|
||||
}
|
||||
|
||||
Assert.False(result.Value.IsFollowing);
|
||||
Assert.True(result.Value.CanViewFollowers);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_UserMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new GetUserProfileQuery(404), TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UserMissing_ReturnsNotFound()
|
||||
{
|
||||
var result = await CreateHandler().Handle(new GetUserProfileQuery(404), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
Assert.Equal(ErrorType.NotFound, result.FirstError.Type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,46 +3,45 @@ using Musify.Application.Users;
|
||||
using Musify.Domain.Entities;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Users
|
||||
namespace Musify.Application.Tests.Users;
|
||||
|
||||
public sealed class GetUsersQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class GetUsersQueryHandlerTests : HandlerTestBase
|
||||
private GetUsersQueryHandler CreateHandler() => new(Database);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NoSearch_ReturnsEveryUser()
|
||||
{
|
||||
private GetUsersQueryHandler CreateHandler() => new(Database);
|
||||
await SeedAsync(TestEntities.User(1, "alice"), TestEntities.User(2, "bob"));
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NoSearch_ReturnsEveryUser()
|
||||
{
|
||||
await SeedAsync(TestEntities.User(1, "alice"), TestEntities.User(2, "bob"));
|
||||
var result = await CreateHandler().Handle(new GetUsersQuery(PageNumber: 1, PageSize: 10, UsernameSearch: null), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetUsersQuery(PageNumber: 1, PageSize: 10, UsernameSearch: null), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(2, result.Value.TotalItemCount);
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal(2, result.Value.TotalItemCount);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_UsernameSearch_ReturnsOnlyMatches()
|
||||
{
|
||||
await SeedAsync(TestEntities.User(1, "alice-wonderland"), TestEntities.User(2, "bob-builder"));
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UsernameSearch_ReturnsOnlyMatches()
|
||||
{
|
||||
await SeedAsync(TestEntities.User(1, "alice-wonderland"), TestEntities.User(2, "bob-builder"));
|
||||
var result = await CreateHandler().Handle(new GetUsersQuery(PageNumber: 1, PageSize: 10, UsernameSearch: "alice"), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetUsersQuery(PageNumber: 1, PageSize: 10, UsernameSearch: "alice"), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
var user = Assert.Single(result.Value.Items);
|
||||
Assert.Equal(1, user.Id);
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
var user = Assert.Single(result.Value.Items);
|
||||
Assert.Equal(1, user.Id);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_Viewer_MarksFollowedUsers()
|
||||
{
|
||||
await SeedAsync(
|
||||
TestEntities.User(1, "viewer"), TestEntities.User(2, "alice"), TestEntities.User(3, "bob"),
|
||||
new UserFollow { FollowerId = 1, FollowedId = 2 });
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_Viewer_MarksFollowedUsers()
|
||||
{
|
||||
await SeedAsync(
|
||||
TestEntities.User(1, "viewer"), TestEntities.User(2, "alice"), TestEntities.User(3, "bob"),
|
||||
new UserFollow { FollowerId = 1, FollowedId = 2 });
|
||||
var result = await CreateHandler().Handle(new GetUsersQuery(PageNumber: 1, PageSize: 10, UsernameSearch: null, ViewerId: 1), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new GetUsersQuery(PageNumber: 1, PageSize: 10, UsernameSearch: null, ViewerId: 1), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.All(result.Value.Items, item => Assert.Equal(item.Id == 2, item.IsFollowedByViewer));
|
||||
}
|
||||
Assert.False(result.IsError);
|
||||
Assert.All(result.Value.Items, item => Assert.Equal(item.Id == 2, item.IsFollowedByViewer));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,34 +3,33 @@ using Musify.Application.Users;
|
||||
using Musify.Domain.Entities;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Users
|
||||
namespace Musify.Application.Tests.Users;
|
||||
|
||||
public sealed class IsFollowingUserQueryHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class IsFollowingUserQueryHandlerTests : HandlerTestBase
|
||||
private IsFollowingUserQueryHandler CreateHandler() => new(Database);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_Following_ReturnsTrue()
|
||||
{
|
||||
private IsFollowingUserQueryHandler CreateHandler() => new(Database);
|
||||
var follower = TestEntities.User(1, "follower");
|
||||
var followed = TestEntities.User(2, "followed");
|
||||
await SeedAsync(follower, followed, new UserFollow { FollowerId = follower.Id, FollowedId = followed.Id });
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_Following_ReturnsTrue()
|
||||
{
|
||||
var follower = TestEntities.User(1, "follower");
|
||||
var followed = TestEntities.User(2, "followed");
|
||||
await SeedAsync(follower, followed, new UserFollow { FollowerId = follower.Id, FollowedId = followed.Id });
|
||||
var result = await CreateHandler().Handle(new IsFollowingUserQuery(follower.Id, followed.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new IsFollowingUserQuery(follower.Id, followed.Id), TestContext.Current.CancellationToken);
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
Assert.True(result);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_NotFollowing_ReturnsFalse()
|
||||
{
|
||||
var follower = TestEntities.User(1, "follower");
|
||||
var followed = TestEntities.User(2, "followed");
|
||||
await SeedAsync(follower, followed);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NotFollowing_ReturnsFalse()
|
||||
{
|
||||
var follower = TestEntities.User(1, "follower");
|
||||
var followed = TestEntities.User(2, "followed");
|
||||
await SeedAsync(follower, followed);
|
||||
var result = await CreateHandler().Handle(new IsFollowingUserQuery(follower.Id, followed.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new IsFollowingUserQuery(follower.Id, followed.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.False(result);
|
||||
}
|
||||
Assert.False(result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,42 +2,41 @@ using Musify.Application.Tests.TestSupport;
|
||||
using Musify.Application.Users;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Users
|
||||
namespace Musify.Application.Tests.Users;
|
||||
|
||||
public sealed class SyncUserCommandHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class SyncUserCommandHandlerTests : HandlerTestBase
|
||||
private SyncUserCommandHandler CreateHandler() => new(Database, NoOpLogger<SyncUserCommandHandler>());
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UnknownUser_ProvisionsIt()
|
||||
{
|
||||
private SyncUserCommandHandler CreateHandler() => new(Database, NoOpLogger<SyncUserCommandHandler>());
|
||||
var command = new SyncUserCommand(1, "Jane Doe", "Jane", "Doe", "https://avatar.example/jane.png");
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UnknownUser_ProvisionsIt()
|
||||
{
|
||||
var command = new SyncUserCommand(1, "Jane Doe", "Jane", "Doe", "https://avatar.example/jane.png");
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
var stored = await Database.Users.FindAsync([1L], TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(stored);
|
||||
Assert.Equal("Jane Doe", stored.Name);
|
||||
Assert.Equal("https://avatar.example/jane.png", stored.ProfilePictureUrl);
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
var stored = await Database.Users.FindAsync([1L], TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(stored);
|
||||
Assert.Equal("Jane Doe", stored.Name);
|
||||
Assert.Equal("https://avatar.example/jane.png", stored.ProfilePictureUrl);
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_ExistingUser_UpdatesProfileFields()
|
||||
{
|
||||
var user = TestEntities.User(1, "Old Name", profilePictureUrl: null);
|
||||
await SeedAsync(user);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ExistingUser_UpdatesProfileFields()
|
||||
{
|
||||
var user = TestEntities.User(1, "Old Name", profilePictureUrl: null);
|
||||
await SeedAsync(user);
|
||||
var command = new SyncUserCommand(1, "New Name", "New", "Name", "https://avatar.example/new.png");
|
||||
|
||||
var command = new SyncUserCommand(1, "New Name", "New", "Name", "https://avatar.example/new.png");
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
var stored = await Database.Users.FindAsync([1L], TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(stored);
|
||||
Assert.Equal("New Name", stored.Name);
|
||||
Assert.Equal("NEW NAME", stored.NormalizedName);
|
||||
Assert.Equal("https://avatar.example/new.png", stored.ProfilePictureUrl);
|
||||
}
|
||||
Assert.False(result.IsError);
|
||||
var stored = await Database.Users.FindAsync([1L], TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(stored);
|
||||
Assert.Equal("New Name", stored.Name);
|
||||
Assert.Equal("NEW NAME", stored.NormalizedName);
|
||||
Assert.Equal("https://avatar.example/new.png", stored.ProfilePictureUrl);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,35 +4,34 @@ using Musify.Application.Users;
|
||||
using Musify.Domain.Entities;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Application.Tests.Users
|
||||
namespace Musify.Application.Tests.Users;
|
||||
|
||||
public sealed class UnfollowUserCommandHandlerTests : HandlerTestBase
|
||||
{
|
||||
public sealed class UnfollowUserCommandHandlerTests : HandlerTestBase
|
||||
private UnfollowUserCommandHandler CreateHandler() => new(Database, NoOpLogger<UnfollowUserCommandHandler>());
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ExistingFollow_RemovesIt()
|
||||
{
|
||||
private UnfollowUserCommandHandler CreateHandler() => new(Database, NoOpLogger<UnfollowUserCommandHandler>());
|
||||
var follower = TestEntities.User(1, "follower");
|
||||
var followed = TestEntities.User(2, "followed");
|
||||
await SeedAsync(follower, followed, new UserFollow { FollowerId = follower.Id, FollowedId = followed.Id });
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ExistingFollow_RemovesIt()
|
||||
{
|
||||
var follower = TestEntities.User(1, "follower");
|
||||
var followed = TestEntities.User(2, "followed");
|
||||
await SeedAsync(follower, followed, new UserFollow { FollowerId = follower.Id, FollowedId = followed.Id });
|
||||
var result = await CreateHandler().Handle(new UnfollowUserCommand(follower.Id, followed.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new UnfollowUserCommand(follower.Id, followed.Id), TestContext.Current.CancellationToken);
|
||||
Assert.False(result.IsError);
|
||||
Assert.Empty(await Database.UserFollows.ToListAsync(TestContext.Current.CancellationToken));
|
||||
}
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Empty(await Database.UserFollows.ToListAsync(TestContext.Current.CancellationToken));
|
||||
}
|
||||
[Fact]
|
||||
public async Task Handle_NotFollowing_IsIdempotent()
|
||||
{
|
||||
var follower = TestEntities.User(1, "follower");
|
||||
var followed = TestEntities.User(2, "followed");
|
||||
await SeedAsync(follower, followed);
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_NotFollowing_IsIdempotent()
|
||||
{
|
||||
var follower = TestEntities.User(1, "follower");
|
||||
var followed = TestEntities.User(2, "followed");
|
||||
await SeedAsync(follower, followed);
|
||||
var result = await CreateHandler().Handle(new UnfollowUserCommand(follower.Id, followed.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
var result = await CreateHandler().Handle(new UnfollowUserCommand(follower.Id, followed.Id), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
}
|
||||
Assert.False(result.IsError);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,51 +1,50 @@
|
||||
using Musify.Domain.ValueObjects;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Domain.Tests.ValueObjects
|
||||
namespace Musify.Domain.Tests.ValueObjects;
|
||||
|
||||
public sealed class GenreCompatibilityTests
|
||||
{
|
||||
public sealed class GenreCompatibilityTests
|
||||
[Theory]
|
||||
[InlineData(Genre.Classical, Genre.Metal)]
|
||||
[InlineData(Genre.Classical, Genre.Punk)]
|
||||
[InlineData(Genre.Ambient, Genre.Dubstep)]
|
||||
[InlineData(Genre.Lofi, Genre.DrumAndBass)]
|
||||
public void AreCompatible_IncompatiblePair_ReturnsFalse(Genre first, Genre second)
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(Genre.Classical, Genre.Metal)]
|
||||
[InlineData(Genre.Classical, Genre.Punk)]
|
||||
[InlineData(Genre.Ambient, Genre.Dubstep)]
|
||||
[InlineData(Genre.Lofi, Genre.DrumAndBass)]
|
||||
public void AreCompatible_IncompatiblePair_ReturnsFalse(Genre first, Genre second)
|
||||
{
|
||||
Assert.False(GenreCompatibility.AreCompatible(first, second));
|
||||
Assert.False(GenreCompatibility.AreCompatible(second, first));
|
||||
}
|
||||
Assert.False(GenreCompatibility.AreCompatible(first, second));
|
||||
Assert.False(GenreCompatibility.AreCompatible(second, first));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(Genre.Pop, Genre.Rock)]
|
||||
[InlineData(Genre.HipHop, Genre.RnB)]
|
||||
[InlineData(Genre.Classical, Genre.Jazz)]
|
||||
public void AreCompatible_CompatiblePair_ReturnsTrue(Genre first, Genre second)
|
||||
{
|
||||
Assert.True(GenreCompatibility.AreCompatible(first, second));
|
||||
}
|
||||
[Theory]
|
||||
[InlineData(Genre.Pop, Genre.Rock)]
|
||||
[InlineData(Genre.HipHop, Genre.RnB)]
|
||||
[InlineData(Genre.Classical, Genre.Jazz)]
|
||||
public void AreCompatible_CompatiblePair_ReturnsTrue(Genre first, Genre second)
|
||||
{
|
||||
Assert.True(GenreCompatibility.AreCompatible(first, second));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AreCompatible_SameGenre_ReturnsTrue()
|
||||
{
|
||||
Assert.True(GenreCompatibility.AreCompatible(Genre.Pop, Genre.Pop));
|
||||
}
|
||||
[Fact]
|
||||
public void AreCompatible_SameGenre_ReturnsTrue()
|
||||
{
|
||||
Assert.True(GenreCompatibility.AreCompatible(Genre.Pop, Genre.Pop));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindConflicts_NoIncompatiblePairs_ReturnsEmpty()
|
||||
{
|
||||
var conflicts = GenreCompatibility.FindConflicts([Genre.Pop, Genre.Rock, Genre.Indie]);
|
||||
[Fact]
|
||||
public void FindConflicts_NoIncompatiblePairs_ReturnsEmpty()
|
||||
{
|
||||
var conflicts = GenreCompatibility.FindConflicts([Genre.Pop, Genre.Rock, Genre.Indie]);
|
||||
|
||||
Assert.Empty(conflicts);
|
||||
}
|
||||
Assert.Empty(conflicts);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindConflicts_IncompatiblePairPresent_ReturnsIt()
|
||||
{
|
||||
var conflicts = GenreCompatibility.FindConflicts([Genre.Pop, Genre.Classical, Genre.Metal]);
|
||||
[Fact]
|
||||
public void FindConflicts_IncompatiblePairPresent_ReturnsIt()
|
||||
{
|
||||
var conflicts = GenreCompatibility.FindConflicts([Genre.Pop, Genre.Classical, Genre.Metal]);
|
||||
|
||||
var conflict = Assert.Single(conflicts);
|
||||
Assert.Equal((Genre.Classical, Genre.Metal), conflict);
|
||||
}
|
||||
var conflict = Assert.Single(conflicts);
|
||||
Assert.Equal((Genre.Classical, Genre.Metal), conflict);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
using Musify.Domain.ValueObjects;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Domain.Tests.ValueObjects
|
||||
{
|
||||
public sealed class TrackAudioTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(ProcessingStatus.Pending, false, false, true)]
|
||||
[InlineData(ProcessingStatus.Processing, false, false, true)]
|
||||
[InlineData(ProcessingStatus.Completed, true, false, false)]
|
||||
[InlineData(ProcessingStatus.Failed, false, true, false)]
|
||||
public void ComputedFlags_ReflectTranscodeStatus(
|
||||
ProcessingStatus status, bool expectedProcessed, bool expectedFailed, bool expectedInProgress)
|
||||
{
|
||||
var audio = new TrackAudio { TranscodeStatus = status };
|
||||
namespace Musify.Domain.Tests.ValueObjects;
|
||||
|
||||
Assert.Equal(expectedProcessed, audio.IsProcessed);
|
||||
Assert.Equal(expectedFailed, audio.IsFailed);
|
||||
Assert.Equal(expectedInProgress, audio.IsInProgress);
|
||||
}
|
||||
public sealed class TrackAudioTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(ProcessingStatus.Pending, false, false, true)]
|
||||
[InlineData(ProcessingStatus.Processing, false, false, true)]
|
||||
[InlineData(ProcessingStatus.Completed, true, false, false)]
|
||||
[InlineData(ProcessingStatus.Failed, false, true, false)]
|
||||
public void ComputedFlags_ReflectTranscodeStatus(
|
||||
ProcessingStatus status, bool expectedProcessed, bool expectedFailed, bool expectedInProgress)
|
||||
{
|
||||
var audio = new TrackAudio { TranscodeStatus = status };
|
||||
|
||||
Assert.Equal(expectedProcessed, audio.IsProcessed);
|
||||
Assert.Equal(expectedFailed, audio.IsFailed);
|
||||
Assert.Equal(expectedInProgress, audio.IsInProgress);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
using Musify.Domain.ValueObjects;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Domain.Tests.ValueObjects
|
||||
{
|
||||
public sealed class TrackPicturesTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(ProcessingStatus.Pending, false)]
|
||||
[InlineData(ProcessingStatus.Processing, false)]
|
||||
[InlineData(ProcessingStatus.Completed, true)]
|
||||
[InlineData(ProcessingStatus.Failed, false)]
|
||||
public void IsProcessed_ReflectsProcessingStatus(ProcessingStatus status, bool expected)
|
||||
{
|
||||
var pictures = new TrackPictures { ProcessingStatus = status };
|
||||
namespace Musify.Domain.Tests.ValueObjects;
|
||||
|
||||
Assert.Equal(expected, pictures.IsProcessed);
|
||||
}
|
||||
public sealed class TrackPicturesTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(ProcessingStatus.Pending, false)]
|
||||
[InlineData(ProcessingStatus.Processing, false)]
|
||||
[InlineData(ProcessingStatus.Completed, true)]
|
||||
[InlineData(ProcessingStatus.Failed, false)]
|
||||
public void IsProcessed_ReflectsProcessingStatus(ProcessingStatus status, bool expected)
|
||||
{
|
||||
var pictures = new TrackPictures { ProcessingStatus = status };
|
||||
|
||||
Assert.Equal(expected, pictures.IsProcessed);
|
||||
}
|
||||
}
|
||||
|
||||
+35
-36
@@ -3,52 +3,51 @@ using Musify.Domain.Entities;
|
||||
using Musify.Infrastructure.Tests.TestSupport;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Infrastructure.Tests.Persistence
|
||||
namespace Musify.Infrastructure.Tests.Persistence;
|
||||
|
||||
[Collection(InfrastructureCollection.Name)]
|
||||
public sealed class AuditableEntityInterceptorTests(InfrastructureTestFixture fixture)
|
||||
{
|
||||
[Collection(InfrastructureCollection.Name)]
|
||||
public sealed class AuditableEntityInterceptorTests(InfrastructureTestFixture fixture)
|
||||
[Fact]
|
||||
public async Task SavingChanges_NewEntity_SetsCreatedAtAndUpdatedAtToNow()
|
||||
{
|
||||
[Fact]
|
||||
public async Task SavingChanges_NewEntity_SetsCreatedAtAndUpdatedAtToNow()
|
||||
{
|
||||
await using var database = fixture.CreateDatabase();
|
||||
var before = DateTime.UtcNow;
|
||||
await using var database = fixture.CreateDatabase();
|
||||
var before = DateTime.UtcNow;
|
||||
|
||||
var name = "audit-user";
|
||||
var user = new User { Id = Random.Shared.NextInt64(1, long.MaxValue), Name = name, NormalizedName = name.ToUpperInvariant() };
|
||||
await database.Users.AddAsync(user, TestContext.Current.CancellationToken);
|
||||
await database.SaveChangesAsync(TestContext.Current.CancellationToken);
|
||||
var name = "audit-user";
|
||||
var user = new User { Id = Random.Shared.NextInt64(1, long.MaxValue), Name = name, NormalizedName = name.ToUpperInvariant() };
|
||||
await database.Users.AddAsync(user, TestContext.Current.CancellationToken);
|
||||
await database.SaveChangesAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
var after = DateTime.UtcNow;
|
||||
var after = DateTime.UtcNow;
|
||||
|
||||
Assert.InRange(user.CreatedAt, before, after);
|
||||
Assert.InRange(user.UpdatedAt, before, after);
|
||||
}
|
||||
Assert.InRange(user.CreatedAt, before, after);
|
||||
Assert.InRange(user.UpdatedAt, before, after);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SavingChanges_ModifiedEntity_BumpsUpdatedAtButKeepsCreatedAt()
|
||||
{
|
||||
await using var database = fixture.CreateDatabase();
|
||||
[Fact]
|
||||
public async Task SavingChanges_ModifiedEntity_BumpsUpdatedAtButKeepsCreatedAt()
|
||||
{
|
||||
await using var database = fixture.CreateDatabase();
|
||||
|
||||
var name = "audit-user-2";
|
||||
var user = new User { Id = Random.Shared.NextInt64(1, long.MaxValue), Name = name, NormalizedName = name.ToUpperInvariant() };
|
||||
await database.Users.AddAsync(user, TestContext.Current.CancellationToken);
|
||||
await database.SaveChangesAsync(TestContext.Current.CancellationToken);
|
||||
var originalCreatedAt = user.CreatedAt;
|
||||
var name = "audit-user-2";
|
||||
var user = new User { Id = Random.Shared.NextInt64(1, long.MaxValue), Name = name, NormalizedName = name.ToUpperInvariant() };
|
||||
await database.Users.AddAsync(user, TestContext.Current.CancellationToken);
|
||||
await database.SaveChangesAsync(TestContext.Current.CancellationToken);
|
||||
var originalCreatedAt = user.CreatedAt;
|
||||
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(50), TestContext.Current.CancellationToken);
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(50), TestContext.Current.CancellationToken);
|
||||
|
||||
user.Name = "audit-user-2-renamed";
|
||||
await database.SaveChangesAsync(TestContext.Current.CancellationToken);
|
||||
user.Name = "audit-user-2-renamed";
|
||||
await database.SaveChangesAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(originalCreatedAt, user.CreatedAt);
|
||||
Assert.True(user.UpdatedAt > originalCreatedAt,
|
||||
$"original-created-at={originalCreatedAt:O}, final-updated-at={user.UpdatedAt:O}");
|
||||
Assert.Equal(originalCreatedAt, user.CreatedAt);
|
||||
Assert.True(user.UpdatedAt > originalCreatedAt,
|
||||
$"original-created-at={originalCreatedAt:O}, final-updated-at={user.UpdatedAt:O}");
|
||||
|
||||
await using var reader = fixture.CreateDatabase();
|
||||
var stored = await reader.Users.SingleAsync(u => u.Id == user.Id, TestContext.Current.CancellationToken);
|
||||
Assert.True(stored.UpdatedAt > stored.CreatedAt,
|
||||
$"stored-created-at={stored.CreatedAt:O}, stored-updated-at={stored.UpdatedAt:O}");
|
||||
}
|
||||
await using var reader = fixture.CreateDatabase();
|
||||
var stored = await reader.Users.SingleAsync(u => u.Id == user.Id, TestContext.Current.CancellationToken);
|
||||
Assert.True(stored.UpdatedAt > stored.CreatedAt,
|
||||
$"stored-created-at={stored.CreatedAt:O}, stored-updated-at={stored.UpdatedAt:O}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,105 +4,104 @@ using Musify.Domain.ValueObjects;
|
||||
using Musify.Infrastructure.Tests.TestSupport;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Infrastructure.Tests.Persistence
|
||||
namespace Musify.Infrastructure.Tests.Persistence;
|
||||
|
||||
[Collection(InfrastructureCollection.Name)]
|
||||
public sealed class DatabaseTests(InfrastructureTestFixture fixture)
|
||||
{
|
||||
[Collection(InfrastructureCollection.Name)]
|
||||
public sealed class DatabaseTests(InfrastructureTestFixture fixture)
|
||||
[Fact]
|
||||
public async Task SaveChangesAsync_TrackWithOwnedTypes_RoundTripsThroughPostgres()
|
||||
{
|
||||
[Fact]
|
||||
public async Task SaveChangesAsync_TrackWithOwnedTypes_RoundTripsThroughPostgres()
|
||||
await using var database = fixture.CreateDatabase();
|
||||
|
||||
var userName = "roundtrip-user";
|
||||
var user = new User { Id = Random.Shared.NextInt64(1, long.MaxValue), Name = userName, NormalizedName = userName.ToUpperInvariant() };
|
||||
await database.Users.AddAsync(user, TestContext.Current.CancellationToken);
|
||||
|
||||
var trackTitle = "Round Trip Track";
|
||||
var track = new Track
|
||||
{
|
||||
await using var database = fixture.CreateDatabase();
|
||||
Title = trackTitle,
|
||||
NormalizedTitle = trackTitle.ToUpperInvariant(),
|
||||
DurationSeconds = 123.4,
|
||||
OwnerUserId = user.Id,
|
||||
Owner = user,
|
||||
Pictures = new TrackPictures { OriginalName = "cover.webp", ProcessingStatus = ProcessingStatus.Pending },
|
||||
Audio = new TrackAudio { OriginalName = "song.mp3", TranscodeStatus = ProcessingStatus.Pending }
|
||||
};
|
||||
await database.Tracks.AddAsync(track, TestContext.Current.CancellationToken);
|
||||
await database.SaveChangesAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
var userName = "roundtrip-user";
|
||||
var user = new User { Id = Random.Shared.NextInt64(1, long.MaxValue), Name = userName, NormalizedName = userName.ToUpperInvariant() };
|
||||
await database.Users.AddAsync(user, TestContext.Current.CancellationToken);
|
||||
await using var reloaded = fixture.CreateDatabase();
|
||||
var stored = await reloaded.Tracks.AsNoTracking().SingleAsync(t => t.Id == track.Id, TestContext.Current.CancellationToken);
|
||||
|
||||
var trackTitle = "Round Trip Track";
|
||||
var track = new Track
|
||||
{
|
||||
Title = trackTitle,
|
||||
NormalizedTitle = trackTitle.ToUpperInvariant(),
|
||||
DurationSeconds = 123.4,
|
||||
OwnerUserId = user.Id,
|
||||
Owner = user,
|
||||
Pictures = new TrackPictures { OriginalName = "cover.webp", ProcessingStatus = ProcessingStatus.Pending },
|
||||
Audio = new TrackAudio { OriginalName = "song.mp3", TranscodeStatus = ProcessingStatus.Pending }
|
||||
};
|
||||
await database.Tracks.AddAsync(track, TestContext.Current.CancellationToken);
|
||||
await database.SaveChangesAsync(TestContext.Current.CancellationToken);
|
||||
Assert.Equal(trackTitle, stored.Title);
|
||||
Assert.Equal("cover.webp", stored.Pictures.OriginalName);
|
||||
Assert.Equal("song.mp3", stored.Audio.OriginalName);
|
||||
Assert.Equal(user.Id, stored.OwnerUserId);
|
||||
}
|
||||
|
||||
await using var reloaded = fixture.CreateDatabase();
|
||||
var stored = await reloaded.Tracks.AsNoTracking().SingleAsync(t => t.Id == track.Id, TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task SaveChangesAsync_TrackWithTags_RoundTripsThroughNativePostgresEnum()
|
||||
{
|
||||
await using var database = fixture.CreateDatabase();
|
||||
|
||||
Assert.Equal(trackTitle, stored.Title);
|
||||
Assert.Equal("cover.webp", stored.Pictures.OriginalName);
|
||||
Assert.Equal("song.mp3", stored.Audio.OriginalName);
|
||||
Assert.Equal(user.Id, stored.OwnerUserId);
|
||||
}
|
||||
var userName = "tags-roundtrip-user";
|
||||
var user = new User { Id = Random.Shared.NextInt64(1, long.MaxValue), Name = userName, NormalizedName = userName.ToUpperInvariant() };
|
||||
var track = BuildTrack(user, $"tags-{Guid.NewGuid():N}");
|
||||
track.Tags =
|
||||
[
|
||||
new TrackTag { TrackId = track.Id, Tag = Genre.Pop },
|
||||
new TrackTag { TrackId = track.Id, Tag = Genre.Rock }
|
||||
];
|
||||
|
||||
[Fact]
|
||||
public async Task SaveChangesAsync_TrackWithTags_RoundTripsThroughNativePostgresEnum()
|
||||
await database.AddRangeAsync(user, track);
|
||||
await database.SaveChangesAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
await using var reloaded = fixture.CreateDatabase();
|
||||
var stored = await reloaded.Tracks.AsNoTracking()
|
||||
.Include(t => t.Tags)
|
||||
.SingleAsync(t => t.Id == track.Id, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal([Genre.Pop, Genre.Rock], stored.Tags.Select(t => t.Tag).OrderBy(t => t));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AlbumHasTracks_DeletingAlbum_CascadesToTheLinkRows()
|
||||
{
|
||||
await using var database = fixture.CreateDatabase();
|
||||
|
||||
var userName = "cascade-user";
|
||||
var user = new User { Id = Random.Shared.NextInt64(1, long.MaxValue), Name = userName, NormalizedName = userName.ToUpperInvariant() };
|
||||
var albumTitle = "Cascade Album";
|
||||
var album = new Album { Title = albumTitle, NormalizedTitle = albumTitle.ToUpperInvariant(), OwnerUserId = user.Id };
|
||||
var track = BuildTrack(user, $"cascade-{Guid.NewGuid():N}");
|
||||
var link = new AlbumHasTrack { AlbumId = album.Id, TrackId = track.Id, TrackNumber = 1 };
|
||||
|
||||
await database.AddRangeAsync(user, album, track, link);
|
||||
await database.SaveChangesAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
await using var deleter = fixture.CreateDatabase();
|
||||
var albumToDelete = await deleter.Albums.SingleAsync(a => a.Id == album.Id, TestContext.Current.CancellationToken);
|
||||
deleter.Albums.Remove(albumToDelete);
|
||||
await deleter.SaveChangesAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
await using var verifier = fixture.CreateDatabase();
|
||||
Assert.False(await verifier.AlbumHasTracks.AnyAsync(l => l.AlbumId == album.Id, TestContext.Current.CancellationToken));
|
||||
Assert.True(await verifier.Tracks.AnyAsync(t => t.Id == track.Id, TestContext.Current.CancellationToken));
|
||||
}
|
||||
|
||||
private static Track BuildTrack(User owner, string title)
|
||||
{
|
||||
return new Track
|
||||
{
|
||||
await using var database = fixture.CreateDatabase();
|
||||
|
||||
var userName = "tags-roundtrip-user";
|
||||
var user = new User { Id = Random.Shared.NextInt64(1, long.MaxValue), Name = userName, NormalizedName = userName.ToUpperInvariant() };
|
||||
var track = BuildTrack(user, $"tags-{Guid.NewGuid():N}");
|
||||
track.Tags =
|
||||
[
|
||||
new TrackTag { TrackId = track.Id, Tag = Genre.Pop },
|
||||
new TrackTag { TrackId = track.Id, Tag = Genre.Rock }
|
||||
];
|
||||
|
||||
await database.AddRangeAsync(user, track);
|
||||
await database.SaveChangesAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
await using var reloaded = fixture.CreateDatabase();
|
||||
var stored = await reloaded.Tracks.AsNoTracking()
|
||||
.Include(t => t.Tags)
|
||||
.SingleAsync(t => t.Id == track.Id, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal([Genre.Pop, Genre.Rock], stored.Tags.Select(t => t.Tag).OrderBy(t => t));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AlbumHasTracks_DeletingAlbum_CascadesToTheLinkRows()
|
||||
{
|
||||
await using var database = fixture.CreateDatabase();
|
||||
|
||||
var userName = "cascade-user";
|
||||
var user = new User { Id = Random.Shared.NextInt64(1, long.MaxValue), Name = userName, NormalizedName = userName.ToUpperInvariant() };
|
||||
var albumTitle = "Cascade Album";
|
||||
var album = new Album { Title = albumTitle, NormalizedTitle = albumTitle.ToUpperInvariant(), OwnerUserId = user.Id };
|
||||
var track = BuildTrack(user, $"cascade-{Guid.NewGuid():N}");
|
||||
var link = new AlbumHasTrack { AlbumId = album.Id, TrackId = track.Id, TrackNumber = 1 };
|
||||
|
||||
await database.AddRangeAsync(user, album, track, link);
|
||||
await database.SaveChangesAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
await using var deleter = fixture.CreateDatabase();
|
||||
var albumToDelete = await deleter.Albums.SingleAsync(a => a.Id == album.Id, TestContext.Current.CancellationToken);
|
||||
deleter.Albums.Remove(albumToDelete);
|
||||
await deleter.SaveChangesAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
await using var verifier = fixture.CreateDatabase();
|
||||
Assert.False(await verifier.AlbumHasTracks.AnyAsync(l => l.AlbumId == album.Id, TestContext.Current.CancellationToken));
|
||||
Assert.True(await verifier.Tracks.AnyAsync(t => t.Id == track.Id, TestContext.Current.CancellationToken));
|
||||
}
|
||||
|
||||
private static Track BuildTrack(User owner, string title)
|
||||
{
|
||||
return new Track
|
||||
{
|
||||
Title = title,
|
||||
NormalizedTitle = title.ToUpperInvariant(),
|
||||
DurationSeconds = 100,
|
||||
OwnerUserId = owner.Id,
|
||||
Owner = owner,
|
||||
Pictures = new TrackPictures(),
|
||||
Audio = new TrackAudio()
|
||||
};
|
||||
}
|
||||
Title = title,
|
||||
NormalizedTitle = title.ToUpperInvariant(),
|
||||
DurationSeconds = 100,
|
||||
OwnerUserId = owner.Id,
|
||||
Owner = owner,
|
||||
Pictures = new TrackPictures(),
|
||||
Audio = new TrackAudio()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,55 +4,54 @@ using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Infrastructure.Tests.Services
|
||||
namespace Musify.Infrastructure.Tests.Services;
|
||||
|
||||
public sealed class PictureServiceTests
|
||||
{
|
||||
public sealed class PictureServiceTests
|
||||
private static PictureService CreateService() => new(NullLogger<PictureService>.Instance);
|
||||
|
||||
private static MemoryStream CreateSourceImage(int width, int height)
|
||||
{
|
||||
private static PictureService CreateService() => new(NullLogger<PictureService>.Instance);
|
||||
using var image = new Image<Rgba32>(width, height, new Rgba32(255, 0, 0));
|
||||
var stream = new MemoryStream();
|
||||
image.SaveAsPng(stream);
|
||||
stream.Position = 0;
|
||||
return stream;
|
||||
}
|
||||
|
||||
private static MemoryStream CreateSourceImage(int width, int height)
|
||||
{
|
||||
using var image = new Image<Rgba32>(width, height, new Rgba32(255, 0, 0));
|
||||
var stream = new MemoryStream();
|
||||
image.SaveAsPng(stream);
|
||||
stream.Position = 0;
|
||||
return stream;
|
||||
}
|
||||
[Fact]
|
||||
public async Task ResizePictureAsWebpAsync_LargerThanTarget_ShrinksToFitWithinTheRequestedBox()
|
||||
{
|
||||
using var source = CreateSourceImage(800, 600);
|
||||
|
||||
[Fact]
|
||||
public async Task ResizePictureAsWebpAsync_LargerThanTarget_ShrinksToFitWithinTheRequestedBox()
|
||||
{
|
||||
using var source = CreateSourceImage(800, 600);
|
||||
await using var resized = await CreateService().ResizePictureAsWebpAsync(source, 200, 200, TestContext.Current.CancellationToken);
|
||||
|
||||
await using var resized = await CreateService().ResizePictureAsWebpAsync(source, 200, 200, TestContext.Current.CancellationToken);
|
||||
using var result = await Image.LoadAsync(resized, TestContext.Current.CancellationToken);
|
||||
Assert.InRange(result.Width, 1, 200);
|
||||
Assert.InRange(result.Height, 1, 200);
|
||||
Assert.True(result.Width == 200 || result.Height == 200, "expected the longer side to land exactly on the target box");
|
||||
}
|
||||
|
||||
using var result = await Image.LoadAsync(resized, TestContext.Current.CancellationToken);
|
||||
Assert.InRange(result.Width, 1, 200);
|
||||
Assert.InRange(result.Height, 1, 200);
|
||||
Assert.True(result.Width == 200 || result.Height == 200, "expected the longer side to land exactly on the target box");
|
||||
}
|
||||
[Fact]
|
||||
public async Task ResizePictureAsWebpAsync_ReturnsAValidWebpStream()
|
||||
{
|
||||
using var source = CreateSourceImage(100, 100);
|
||||
|
||||
[Fact]
|
||||
public async Task ResizePictureAsWebpAsync_ReturnsAValidWebpStream()
|
||||
{
|
||||
using var source = CreateSourceImage(100, 100);
|
||||
await using var resized = await CreateService().ResizePictureAsWebpAsync(source, 50, 50, TestContext.Current.CancellationToken);
|
||||
|
||||
await using var resized = await CreateService().ResizePictureAsWebpAsync(source, 50, 50, TestContext.Current.CancellationToken);
|
||||
var format = await Image.DetectFormatAsync(resized, TestContext.Current.CancellationToken);
|
||||
Assert.Equal("WEBP", format?.Name, ignoreCase: true);
|
||||
}
|
||||
|
||||
var format = await Image.DetectFormatAsync(resized, TestContext.Current.CancellationToken);
|
||||
Assert.Equal("WEBP", format?.Name, ignoreCase: true);
|
||||
}
|
||||
[Fact]
|
||||
public async Task ResizePictureAsWebpAsync_PreservesAspectRatio()
|
||||
{
|
||||
using var source = CreateSourceImage(400, 200);
|
||||
|
||||
[Fact]
|
||||
public async Task ResizePictureAsWebpAsync_PreservesAspectRatio()
|
||||
{
|
||||
using var source = CreateSourceImage(400, 200);
|
||||
await using var resized = await CreateService().ResizePictureAsWebpAsync(source, 100, 100, TestContext.Current.CancellationToken);
|
||||
|
||||
await using var resized = await CreateService().ResizePictureAsWebpAsync(source, 100, 100, TestContext.Current.CancellationToken);
|
||||
|
||||
using var result = await Image.LoadAsync(resized, TestContext.Current.CancellationToken);
|
||||
Assert.Equal(100, result.Width);
|
||||
Assert.Equal(50, result.Height);
|
||||
}
|
||||
using var result = await Image.LoadAsync(resized, TestContext.Current.CancellationToken);
|
||||
Assert.Equal(100, result.Width);
|
||||
Assert.Equal(50, result.Height);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,79 +3,78 @@ using Microsoft.Extensions.Caching.Memory;
|
||||
using Musify.Infrastructure.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Infrastructure.Tests.Services
|
||||
namespace Musify.Infrastructure.Tests.Services;
|
||||
|
||||
public sealed class SingleFlightCacheTests
|
||||
{
|
||||
public sealed class SingleFlightCacheTests
|
||||
private static SingleFlightCache CreateCache() => new(new MemoryCache(new MemoryCacheOptions()));
|
||||
|
||||
[Fact]
|
||||
public async Task GetOrCreateAsync_SecondCallWithinDuration_ReturnsCachedValueWithoutCallingFactory()
|
||||
{
|
||||
private static SingleFlightCache CreateCache() => new(new MemoryCache(new MemoryCacheOptions()));
|
||||
var cache = CreateCache();
|
||||
var calls = 0;
|
||||
Task<ErrorOr<int>> Factory() { calls++; return Task.FromResult<ErrorOr<int>>(calls); }
|
||||
|
||||
[Fact]
|
||||
public async Task GetOrCreateAsync_SecondCallWithinDuration_ReturnsCachedValueWithoutCallingFactory()
|
||||
var first = await cache.GetOrCreateAsync("key", TimeSpan.FromMinutes(1), Factory);
|
||||
var second = await cache.GetOrCreateAsync("key", TimeSpan.FromMinutes(1), Factory);
|
||||
|
||||
Assert.Equal(1, first.Value);
|
||||
Assert.Equal(1, second.Value);
|
||||
Assert.Equal(1, calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetOrCreateAsync_ConcurrentCallsForTheSameKey_CollapseIntoASingleFactoryCall()
|
||||
{
|
||||
var cache = CreateCache();
|
||||
var calls = 0;
|
||||
var gate = new TaskCompletionSource();
|
||||
|
||||
async Task<ErrorOr<int>> Factory()
|
||||
{
|
||||
var cache = CreateCache();
|
||||
var calls = 0;
|
||||
Task<ErrorOr<int>> Factory() { calls++; return Task.FromResult<ErrorOr<int>>(calls); }
|
||||
|
||||
var first = await cache.GetOrCreateAsync("key", TimeSpan.FromMinutes(1), Factory);
|
||||
var second = await cache.GetOrCreateAsync("key", TimeSpan.FromMinutes(1), Factory);
|
||||
|
||||
Assert.Equal(1, first.Value);
|
||||
Assert.Equal(1, second.Value);
|
||||
Assert.Equal(1, calls);
|
||||
Interlocked.Increment(ref calls);
|
||||
await gate.Task;
|
||||
return 7;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetOrCreateAsync_ConcurrentCallsForTheSameKey_CollapseIntoASingleFactoryCall()
|
||||
var first = cache.GetOrCreateAsync("concurrent-key", TimeSpan.FromMinutes(1), Factory);
|
||||
var second = cache.GetOrCreateAsync("concurrent-key", TimeSpan.FromMinutes(1), Factory);
|
||||
gate.SetResult();
|
||||
|
||||
var results = await Task.WhenAll(first, second);
|
||||
|
||||
Assert.Equal(1, calls);
|
||||
Assert.All(results, result => Assert.Equal(7, result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetOrCreateAsync_FactoryReturnsError_DoesNotCacheIt()
|
||||
{
|
||||
var cache = CreateCache();
|
||||
var calls = 0;
|
||||
Task<ErrorOr<int>> Factory()
|
||||
{
|
||||
var cache = CreateCache();
|
||||
var calls = 0;
|
||||
var gate = new TaskCompletionSource();
|
||||
|
||||
async Task<ErrorOr<int>> Factory()
|
||||
{
|
||||
Interlocked.Increment(ref calls);
|
||||
await gate.Task;
|
||||
return 7;
|
||||
}
|
||||
|
||||
var first = cache.GetOrCreateAsync("concurrent-key", TimeSpan.FromMinutes(1), Factory);
|
||||
var second = cache.GetOrCreateAsync("concurrent-key", TimeSpan.FromMinutes(1), Factory);
|
||||
gate.SetResult();
|
||||
|
||||
var results = await Task.WhenAll(first, second);
|
||||
|
||||
Assert.Equal(1, calls);
|
||||
Assert.All(results, result => Assert.Equal(7, result.Value));
|
||||
calls++;
|
||||
return Task.FromResult<ErrorOr<int>>(Error.Failure(description: "boom"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetOrCreateAsync_FactoryReturnsError_DoesNotCacheIt()
|
||||
{
|
||||
var cache = CreateCache();
|
||||
var calls = 0;
|
||||
Task<ErrorOr<int>> Factory()
|
||||
{
|
||||
calls++;
|
||||
return Task.FromResult<ErrorOr<int>>(Error.Failure(description: "boom"));
|
||||
}
|
||||
await cache.GetOrCreateAsync("error-key", TimeSpan.FromMinutes(1), Factory);
|
||||
await cache.GetOrCreateAsync("error-key", TimeSpan.FromMinutes(1), Factory);
|
||||
|
||||
await cache.GetOrCreateAsync("error-key", TimeSpan.FromMinutes(1), Factory);
|
||||
await cache.GetOrCreateAsync("error-key", TimeSpan.FromMinutes(1), Factory);
|
||||
Assert.Equal(2, calls);
|
||||
}
|
||||
|
||||
Assert.Equal(2, calls);
|
||||
}
|
||||
[Fact]
|
||||
public async Task GetOrCreateAsync_NonPositiveDuration_AlwaysCallsTheFactory()
|
||||
{
|
||||
var cache = CreateCache();
|
||||
var calls = 0;
|
||||
Task<ErrorOr<int>> Factory() { calls++; return Task.FromResult<ErrorOr<int>>(calls); }
|
||||
|
||||
[Fact]
|
||||
public async Task GetOrCreateAsync_NonPositiveDuration_AlwaysCallsTheFactory()
|
||||
{
|
||||
var cache = CreateCache();
|
||||
var calls = 0;
|
||||
Task<ErrorOr<int>> Factory() { calls++; return Task.FromResult<ErrorOr<int>>(calls); }
|
||||
await cache.GetOrCreateAsync("no-cache-key", TimeSpan.Zero, Factory);
|
||||
await cache.GetOrCreateAsync("no-cache-key", TimeSpan.Zero, Factory);
|
||||
|
||||
await cache.GetOrCreateAsync("no-cache-key", TimeSpan.Zero, Factory);
|
||||
await cache.GetOrCreateAsync("no-cache-key", TimeSpan.Zero, Factory);
|
||||
|
||||
Assert.Equal(2, calls);
|
||||
}
|
||||
Assert.Equal(2, calls);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,139 +8,138 @@ using Musify.Infrastructure.Services;
|
||||
using Musify.Infrastructure.Tests.TestSupport;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Infrastructure.Tests.Services
|
||||
namespace Musify.Infrastructure.Tests.Services;
|
||||
|
||||
[Collection(InfrastructureCollection.Name)]
|
||||
public sealed class StorageServiceTests(InfrastructureTestFixture fixture) : IAsyncLifetime
|
||||
{
|
||||
[Collection(InfrastructureCollection.Name)]
|
||||
public sealed class StorageServiceTests(InfrastructureTestFixture fixture) : IAsyncLifetime
|
||||
private Database database = null!;
|
||||
private StorageService service = null!;
|
||||
|
||||
public ValueTask InitializeAsync()
|
||||
{
|
||||
private Database database = null!;
|
||||
private StorageService service = null!;
|
||||
|
||||
public ValueTask InitializeAsync()
|
||||
database = fixture.CreateDatabase();
|
||||
var storageConfiguration = new InfrastructureStorageConfiguration
|
||||
{
|
||||
database = fixture.CreateDatabase();
|
||||
var storageConfiguration = new InfrastructureStorageConfiguration
|
||||
{
|
||||
Address = fixture.S3ServiceUrl,
|
||||
AccessKey = InfrastructureTestFixture.S3AccessKey,
|
||||
SecretAccessKey = InfrastructureTestFixture.S3SecretKey,
|
||||
ForcePathStyle = true,
|
||||
UseHttp = true,
|
||||
};
|
||||
service = new StorageService(database, fixture.CreateS3Client(), NullLogger<StorageService>.Instance, storageConfiguration);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
Address = fixture.S3ServiceUrl,
|
||||
AccessKey = InfrastructureTestFixture.S3AccessKey,
|
||||
SecretAccessKey = InfrastructureTestFixture.S3SecretKey,
|
||||
ForcePathStyle = true,
|
||||
UseHttp = true,
|
||||
};
|
||||
service = new StorageService(database, fixture.CreateS3Client(), NullLogger<StorageService>.Instance, storageConfiguration);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync() => database.DisposeAsync();
|
||||
public ValueTask DisposeAsync() => database.DisposeAsync();
|
||||
|
||||
[Fact]
|
||||
public async Task UploadThenGetFile_RoundTripsTheSameBytes()
|
||||
[Fact]
|
||||
public async Task UploadThenGetFile_RoundTripsTheSameBytes()
|
||||
{
|
||||
var key = $"tests/{Guid.NewGuid():N}/round-trip.txt";
|
||||
var content = "hello seaweedfs"u8.ToArray();
|
||||
|
||||
await service.UploadFileAsync(new MemoryStream(content), "text/plain", InfrastructureTestFixture.S3Bucket, key, TestContext.Current.CancellationToken);
|
||||
|
||||
await using var stream = await service.GetFileAsync(InfrastructureTestFixture.S3Bucket, key, TestContext.Current.CancellationToken);
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8);
|
||||
var readBack = await reader.ReadToEndAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal("hello seaweedfs", readBack);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HeadObjectAsync_ExistingObject_ReturnsMetadata()
|
||||
{
|
||||
var key = $"tests/{Guid.NewGuid():N}/head.txt";
|
||||
await service.UploadFileAsync(new MemoryStream("x"u8.ToArray()), "text/plain", InfrastructureTestFixture.S3Bucket, key, TestContext.Current.CancellationToken);
|
||||
|
||||
var metadata = await service.HeadObjectAsync(InfrastructureTestFixture.S3Bucket, key, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.NotNull(metadata);
|
||||
Assert.Equal(1, metadata.ContentLength);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HeadObjectAsync_MissingObject_ReturnsNull()
|
||||
{
|
||||
var metadata = await service.HeadObjectAsync(InfrastructureTestFixture.S3Bucket, $"tests/{Guid.NewGuid():N}/missing.txt", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Null(metadata);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RemoveFileAsync_ExistingObject_DeletesIt()
|
||||
{
|
||||
var key = $"tests/{Guid.NewGuid():N}/to-remove.txt";
|
||||
await service.UploadFileAsync(new MemoryStream("x"u8.ToArray()), "text/plain", InfrastructureTestFixture.S3Bucket, key, TestContext.Current.CancellationToken);
|
||||
|
||||
await service.RemoveFileAsync(InfrastructureTestFixture.S3Bucket, key, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Null(await service.HeadObjectAsync(InfrastructureTestFixture.S3Bucket, key, TestContext.Current.CancellationToken));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CopyFileAsync_CopiesToTheDestinationKey()
|
||||
{
|
||||
var sourceKey = $"tests/{Guid.NewGuid():N}/source.txt";
|
||||
var destinationKey = $"tests/{Guid.NewGuid():N}/destination.txt";
|
||||
await service.UploadFileAsync(new MemoryStream("copy-me"u8.ToArray()), "text/plain", InfrastructureTestFixture.S3Bucket, sourceKey, TestContext.Current.CancellationToken);
|
||||
|
||||
await service.CopyFileAsync(InfrastructureTestFixture.S3Bucket, sourceKey, InfrastructureTestFixture.S3Bucket, destinationKey, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.NotNull(await service.HeadObjectAsync(InfrastructureTestFixture.S3Bucket, destinationKey, TestContext.Current.CancellationToken));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListObjectsAsync_ReturnsEveryObjectUnderThePrefix()
|
||||
{
|
||||
var prefix = $"tests/{Guid.NewGuid():N}";
|
||||
await service.UploadFileAsync(new MemoryStream("a"u8.ToArray()), "text/plain", InfrastructureTestFixture.S3Bucket, $"{prefix}/a.txt", TestContext.Current.CancellationToken);
|
||||
await service.UploadFileAsync(new MemoryStream("b"u8.ToArray()), "text/plain", InfrastructureTestFixture.S3Bucket, $"{prefix}/b.txt", TestContext.Current.CancellationToken);
|
||||
|
||||
var keys = new List<string>();
|
||||
await foreach (var item in service.ListObjectsAsync(InfrastructureTestFixture.S3Bucket, prefix, TestContext.Current.CancellationToken))
|
||||
keys.Add(item.Key);
|
||||
|
||||
Assert.Equal([$"{prefix}/a.txt", $"{prefix}/b.txt"], keys.Order());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RemoveFolderAsync_DeletesEveryObjectUnderThePrefix()
|
||||
{
|
||||
var prefix = $"tests/{Guid.NewGuid():N}";
|
||||
await service.UploadFileAsync(new MemoryStream("a"u8.ToArray()), "text/plain", InfrastructureTestFixture.S3Bucket, $"{prefix}/a.txt", TestContext.Current.CancellationToken);
|
||||
await service.UploadFileAsync(new MemoryStream("b"u8.ToArray()), "text/plain", InfrastructureTestFixture.S3Bucket, $"{prefix}/nested/b.txt", TestContext.Current.CancellationToken);
|
||||
|
||||
await service.RemoveFolderAsync(InfrastructureTestFixture.S3Bucket, prefix, TestContext.Current.CancellationToken);
|
||||
|
||||
var remaining = new List<string>();
|
||||
await foreach (var item in service.ListObjectsAsync(InfrastructureTestFixture.S3Bucket, prefix, TestContext.Current.CancellationToken))
|
||||
remaining.Add(item.Key);
|
||||
Assert.Empty(remaining);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetUploadUrlAsync_PresignedPut_CanActuallyUploadTheObject()
|
||||
{
|
||||
var key = $"tests/{Guid.NewGuid():N}/presigned.txt";
|
||||
|
||||
var uploadUrl = await service.GetUploadUrlAsync(
|
||||
InfrastructureTestFixture.S3Bucket, key, "text/plain", TimeSpan.FromMinutes(5), TestContext.Current.CancellationToken);
|
||||
|
||||
using var httpClient = new HttpClient();
|
||||
using var request = new HttpRequestMessage(HttpMethod.Put, uploadUrl)
|
||||
{
|
||||
var key = $"tests/{Guid.NewGuid():N}/round-trip.txt";
|
||||
var content = "hello seaweedfs"u8.ToArray();
|
||||
Content = new ByteArrayContent(Encoding.UTF8.GetBytes("presigned content"))
|
||||
};
|
||||
request.Content.Headers.ContentType = new MediaTypeHeaderValue("text/plain");
|
||||
request.Headers.TryAddWithoutValidation("If-None-Match", "*");
|
||||
var response = await httpClient.SendAsync(request, TestContext.Current.CancellationToken);
|
||||
|
||||
await service.UploadFileAsync(new MemoryStream(content), "text/plain", InfrastructureTestFixture.S3Bucket, key, TestContext.Current.CancellationToken);
|
||||
|
||||
await using var stream = await service.GetFileAsync(InfrastructureTestFixture.S3Bucket, key, TestContext.Current.CancellationToken);
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8);
|
||||
var readBack = await reader.ReadToEndAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal("hello seaweedfs", readBack);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HeadObjectAsync_ExistingObject_ReturnsMetadata()
|
||||
{
|
||||
var key = $"tests/{Guid.NewGuid():N}/head.txt";
|
||||
await service.UploadFileAsync(new MemoryStream("x"u8.ToArray()), "text/plain", InfrastructureTestFixture.S3Bucket, key, TestContext.Current.CancellationToken);
|
||||
|
||||
var metadata = await service.HeadObjectAsync(InfrastructureTestFixture.S3Bucket, key, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.NotNull(metadata);
|
||||
Assert.Equal(1, metadata.ContentLength);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HeadObjectAsync_MissingObject_ReturnsNull()
|
||||
{
|
||||
var metadata = await service.HeadObjectAsync(InfrastructureTestFixture.S3Bucket, $"tests/{Guid.NewGuid():N}/missing.txt", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Null(metadata);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RemoveFileAsync_ExistingObject_DeletesIt()
|
||||
{
|
||||
var key = $"tests/{Guid.NewGuid():N}/to-remove.txt";
|
||||
await service.UploadFileAsync(new MemoryStream("x"u8.ToArray()), "text/plain", InfrastructureTestFixture.S3Bucket, key, TestContext.Current.CancellationToken);
|
||||
|
||||
await service.RemoveFileAsync(InfrastructureTestFixture.S3Bucket, key, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Null(await service.HeadObjectAsync(InfrastructureTestFixture.S3Bucket, key, TestContext.Current.CancellationToken));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CopyFileAsync_CopiesToTheDestinationKey()
|
||||
{
|
||||
var sourceKey = $"tests/{Guid.NewGuid():N}/source.txt";
|
||||
var destinationKey = $"tests/{Guid.NewGuid():N}/destination.txt";
|
||||
await service.UploadFileAsync(new MemoryStream("copy-me"u8.ToArray()), "text/plain", InfrastructureTestFixture.S3Bucket, sourceKey, TestContext.Current.CancellationToken);
|
||||
|
||||
await service.CopyFileAsync(InfrastructureTestFixture.S3Bucket, sourceKey, InfrastructureTestFixture.S3Bucket, destinationKey, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.NotNull(await service.HeadObjectAsync(InfrastructureTestFixture.S3Bucket, destinationKey, TestContext.Current.CancellationToken));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListObjectsAsync_ReturnsEveryObjectUnderThePrefix()
|
||||
{
|
||||
var prefix = $"tests/{Guid.NewGuid():N}";
|
||||
await service.UploadFileAsync(new MemoryStream("a"u8.ToArray()), "text/plain", InfrastructureTestFixture.S3Bucket, $"{prefix}/a.txt", TestContext.Current.CancellationToken);
|
||||
await service.UploadFileAsync(new MemoryStream("b"u8.ToArray()), "text/plain", InfrastructureTestFixture.S3Bucket, $"{prefix}/b.txt", TestContext.Current.CancellationToken);
|
||||
|
||||
var keys = new List<string>();
|
||||
await foreach (var item in service.ListObjectsAsync(InfrastructureTestFixture.S3Bucket, prefix, TestContext.Current.CancellationToken))
|
||||
keys.Add(item.Key);
|
||||
|
||||
Assert.Equal([$"{prefix}/a.txt", $"{prefix}/b.txt"], keys.Order());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RemoveFolderAsync_DeletesEveryObjectUnderThePrefix()
|
||||
{
|
||||
var prefix = $"tests/{Guid.NewGuid():N}";
|
||||
await service.UploadFileAsync(new MemoryStream("a"u8.ToArray()), "text/plain", InfrastructureTestFixture.S3Bucket, $"{prefix}/a.txt", TestContext.Current.CancellationToken);
|
||||
await service.UploadFileAsync(new MemoryStream("b"u8.ToArray()), "text/plain", InfrastructureTestFixture.S3Bucket, $"{prefix}/nested/b.txt", TestContext.Current.CancellationToken);
|
||||
|
||||
await service.RemoveFolderAsync(InfrastructureTestFixture.S3Bucket, prefix, TestContext.Current.CancellationToken);
|
||||
|
||||
var remaining = new List<string>();
|
||||
await foreach (var item in service.ListObjectsAsync(InfrastructureTestFixture.S3Bucket, prefix, TestContext.Current.CancellationToken))
|
||||
remaining.Add(item.Key);
|
||||
Assert.Empty(remaining);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetUploadUrlAsync_PresignedPut_CanActuallyUploadTheObject()
|
||||
{
|
||||
var key = $"tests/{Guid.NewGuid():N}/presigned.txt";
|
||||
|
||||
var uploadUrl = await service.GetUploadUrlAsync(
|
||||
InfrastructureTestFixture.S3Bucket, key, "text/plain", TimeSpan.FromMinutes(5), TestContext.Current.CancellationToken);
|
||||
|
||||
using var httpClient = new HttpClient();
|
||||
using var request = new HttpRequestMessage(HttpMethod.Put, uploadUrl)
|
||||
{
|
||||
Content = new ByteArrayContent(Encoding.UTF8.GetBytes("presigned content"))
|
||||
};
|
||||
request.Content.Headers.ContentType = new MediaTypeHeaderValue("text/plain");
|
||||
request.Headers.TryAddWithoutValidation("If-None-Match", "*");
|
||||
var response = await httpClient.SendAsync(request, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var metadata = await service.HeadObjectAsync(InfrastructureTestFixture.S3Bucket, key, TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(metadata);
|
||||
Assert.Equal("presigned content".Length, metadata.ContentLength);
|
||||
}
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var metadata = await service.HeadObjectAsync(InfrastructureTestFixture.S3Bucket, key, TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(metadata);
|
||||
Assert.Equal("presigned content".Length, metadata.ContentLength);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,105 +5,104 @@ using Musify.Infrastructure.Configuration;
|
||||
using Musify.Infrastructure.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Infrastructure.Tests.Services
|
||||
namespace Musify.Infrastructure.Tests.Services;
|
||||
|
||||
public sealed class StreamTicketServiceTests : IDisposable
|
||||
{
|
||||
public sealed class StreamTicketServiceTests : IDisposable
|
||||
private readonly string privateKeyPath = Path.GetTempFileName();
|
||||
private readonly RSA rsa = RSA.Create(2048);
|
||||
|
||||
public StreamTicketServiceTests() => File.WriteAllText(privateKeyPath, rsa.ExportPkcs8PrivateKeyPem());
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
private readonly string privateKeyPath = Path.GetTempFileName();
|
||||
private readonly RSA rsa = RSA.Create(2048);
|
||||
rsa.Dispose();
|
||||
File.Delete(privateKeyPath);
|
||||
}
|
||||
|
||||
public StreamTicketServiceTests() => File.WriteAllText(privateKeyPath, rsa.ExportPkcs8PrivateKeyPem());
|
||||
private StreamTicketService CreateService(int ticketTtlSeconds = 3600) => new(new StreamTicketConfiguration
|
||||
{
|
||||
PrivateKeyPath = privateKeyPath,
|
||||
Audience = "media-gateway",
|
||||
Issuer = "musify-webapi",
|
||||
TicketTtlSeconds = ticketTtlSeconds
|
||||
});
|
||||
|
||||
public void Dispose()
|
||||
[Fact]
|
||||
public void IssueTicket_ReturnsATokenSignedWithTheConfiguredKeyAndClaims()
|
||||
{
|
||||
using var service = CreateService(ticketTtlSeconds: 120);
|
||||
|
||||
var ticket = service.IssueTicket(userId: 42, keyPrefix: "Tracks/ProcessedAudios/abc/");
|
||||
|
||||
Assert.Equal(120, ticket.ExpiresInSeconds);
|
||||
|
||||
var handler = new JsonWebTokenHandler();
|
||||
var jwt = handler.ReadJsonWebToken(ticket.Token);
|
||||
Assert.Equal("42", jwt.GetClaim("sub").Value);
|
||||
Assert.Equal("Tracks/ProcessedAudios/abc/", jwt.GetClaim("prefix").Value);
|
||||
Assert.Equal("musify-webapi", jwt.Issuer);
|
||||
Assert.Contains("media-gateway", jwt.Audiences);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IssueTicket_AnonymousUser_OmitsTheSubClaim()
|
||||
{
|
||||
using var service = CreateService();
|
||||
|
||||
var ticket = service.IssueTicket(userId: null, keyPrefix: "Tracks/ProcessedAudios/abc/");
|
||||
|
||||
var handler = new JsonWebTokenHandler();
|
||||
var jwt = handler.ReadJsonWebToken(ticket.Token);
|
||||
Assert.DoesNotContain(jwt.Claims, c => c.Type == "sub");
|
||||
Assert.Equal("Tracks/ProcessedAudios/abc/", jwt.GetClaim("prefix").Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IssueTicket_WithMaxBytes_IncludesTheMaxBytesClaim()
|
||||
{
|
||||
using var service = CreateService();
|
||||
|
||||
var ticket = service.IssueTicket(userId: null, keyPrefix: "Tracks/ProcessedAudios/abc/", maxBytes: 480_000);
|
||||
|
||||
var handler = new JsonWebTokenHandler();
|
||||
var jwt = handler.ReadJsonWebToken(ticket.Token);
|
||||
Assert.Equal("480000", jwt.GetClaim("maxBytes").Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IssueTicket_WithoutMaxBytes_OmitsTheMaxBytesClaim()
|
||||
{
|
||||
using var service = CreateService();
|
||||
|
||||
var ticket = service.IssueTicket(userId: 1, keyPrefix: "Tracks/ProcessedAudios/abc/");
|
||||
|
||||
var handler = new JsonWebTokenHandler();
|
||||
var jwt = handler.ReadJsonWebToken(ticket.Token);
|
||||
Assert.DoesNotContain(jwt.Claims, c => c.Type == "maxBytes");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IssueTicket_TokenValidatesAgainstTheMatchingPublicKey()
|
||||
{
|
||||
using var service = CreateService();
|
||||
var ticket = service.IssueTicket(userId: 1, keyPrefix: "Tracks/ProcessedAudios/xyz/");
|
||||
|
||||
var publicKeyPem = rsa.ExportSubjectPublicKeyInfoPem();
|
||||
using var verifyingRsa = RSA.Create();
|
||||
verifyingRsa.ImportFromPem(publicKeyPem);
|
||||
|
||||
var handler = new JsonWebTokenHandler();
|
||||
var result = await handler.ValidateTokenAsync(ticket.Token, new TokenValidationParameters
|
||||
{
|
||||
rsa.Dispose();
|
||||
File.Delete(privateKeyPath);
|
||||
}
|
||||
|
||||
private StreamTicketService CreateService(int ticketTtlSeconds = 3600) => new(new StreamTicketConfiguration
|
||||
{
|
||||
PrivateKeyPath = privateKeyPath,
|
||||
Audience = "media-gateway",
|
||||
Issuer = "musify-webapi",
|
||||
TicketTtlSeconds = ticketTtlSeconds
|
||||
ValidIssuer = "musify-webapi",
|
||||
ValidAudience = "media-gateway",
|
||||
ValidateIssuer = true,
|
||||
ValidateAudience = true,
|
||||
ValidateLifetime = true,
|
||||
IssuerSigningKey = new RsaSecurityKey(verifyingRsa),
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void IssueTicket_ReturnsATokenSignedWithTheConfiguredKeyAndClaims()
|
||||
{
|
||||
using var service = CreateService(ticketTtlSeconds: 120);
|
||||
|
||||
var ticket = service.IssueTicket(userId: 42, keyPrefix: "Tracks/ProcessedAudios/abc/");
|
||||
|
||||
Assert.Equal(120, ticket.ExpiresInSeconds);
|
||||
|
||||
var handler = new JsonWebTokenHandler();
|
||||
var jwt = handler.ReadJsonWebToken(ticket.Token);
|
||||
Assert.Equal("42", jwt.GetClaim("sub").Value);
|
||||
Assert.Equal("Tracks/ProcessedAudios/abc/", jwt.GetClaim("prefix").Value);
|
||||
Assert.Equal("musify-webapi", jwt.Issuer);
|
||||
Assert.Contains("media-gateway", jwt.Audiences);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IssueTicket_AnonymousUser_OmitsTheSubClaim()
|
||||
{
|
||||
using var service = CreateService();
|
||||
|
||||
var ticket = service.IssueTicket(userId: null, keyPrefix: "Tracks/ProcessedAudios/abc/");
|
||||
|
||||
var handler = new JsonWebTokenHandler();
|
||||
var jwt = handler.ReadJsonWebToken(ticket.Token);
|
||||
Assert.DoesNotContain(jwt.Claims, c => c.Type == "sub");
|
||||
Assert.Equal("Tracks/ProcessedAudios/abc/", jwt.GetClaim("prefix").Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IssueTicket_WithMaxBytes_IncludesTheMaxBytesClaim()
|
||||
{
|
||||
using var service = CreateService();
|
||||
|
||||
var ticket = service.IssueTicket(userId: null, keyPrefix: "Tracks/ProcessedAudios/abc/", maxBytes: 480_000);
|
||||
|
||||
var handler = new JsonWebTokenHandler();
|
||||
var jwt = handler.ReadJsonWebToken(ticket.Token);
|
||||
Assert.Equal("480000", jwt.GetClaim("maxBytes").Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IssueTicket_WithoutMaxBytes_OmitsTheMaxBytesClaim()
|
||||
{
|
||||
using var service = CreateService();
|
||||
|
||||
var ticket = service.IssueTicket(userId: 1, keyPrefix: "Tracks/ProcessedAudios/abc/");
|
||||
|
||||
var handler = new JsonWebTokenHandler();
|
||||
var jwt = handler.ReadJsonWebToken(ticket.Token);
|
||||
Assert.DoesNotContain(jwt.Claims, c => c.Type == "maxBytes");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IssueTicket_TokenValidatesAgainstTheMatchingPublicKey()
|
||||
{
|
||||
using var service = CreateService();
|
||||
var ticket = service.IssueTicket(userId: 1, keyPrefix: "Tracks/ProcessedAudios/xyz/");
|
||||
|
||||
var publicKeyPem = rsa.ExportSubjectPublicKeyInfoPem();
|
||||
using var verifyingRsa = RSA.Create();
|
||||
verifyingRsa.ImportFromPem(publicKeyPem);
|
||||
|
||||
var handler = new JsonWebTokenHandler();
|
||||
var result = await handler.ValidateTokenAsync(ticket.Token, new TokenValidationParameters
|
||||
{
|
||||
ValidIssuer = "musify-webapi",
|
||||
ValidAudience = "media-gateway",
|
||||
ValidateIssuer = true,
|
||||
ValidateAudience = true,
|
||||
ValidateLifetime = true,
|
||||
IssuerSigningKey = new RsaSecurityKey(verifyingRsa),
|
||||
});
|
||||
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
}
|
||||
|
||||
+85
-86
@@ -1,105 +1,104 @@
|
||||
using System.Text.Json;
|
||||
using Amazon.S3;
|
||||
using DotNet.Testcontainers.Builders;
|
||||
using DotNet.Testcontainers.Containers;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Musify.Infrastructure.Configuration;
|
||||
using Musify.Infrastructure.Persistence;
|
||||
using Testcontainers.PostgreSql;
|
||||
using DotNet.Testcontainers.Builders;
|
||||
using DotNet.Testcontainers.Containers;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Infrastructure.Tests.TestSupport
|
||||
namespace Musify.Infrastructure.Tests.TestSupport;
|
||||
|
||||
public sealed class InfrastructureTestFixture : IAsyncLifetime
|
||||
{
|
||||
public sealed class InfrastructureTestFixture : IAsyncLifetime
|
||||
public const string S3AccessKey = "musify-test";
|
||||
public const string S3SecretKey = "musify-test-secret";
|
||||
public const string S3Bucket = "musify-test-bucket";
|
||||
|
||||
private readonly PostgreSqlContainer postgres = new PostgreSqlBuilder("postgres:17-alpine")
|
||||
.WithDatabase("musify_test")
|
||||
.WithUsername("postgres")
|
||||
.WithPassword("postgres")
|
||||
.Build();
|
||||
|
||||
private readonly IContainer seaweedFs = new ContainerBuilder("chrislusf/seaweedfs:latest")
|
||||
.WithEntrypoint("/bin/sh", "-c")
|
||||
.WithCommand(BuildSeaweedFsStartupScript())
|
||||
.WithPortBinding(9333, assignRandomHostPort: true)
|
||||
.WithPortBinding(8333, assignRandomHostPort: true)
|
||||
.WithWaitStrategy(Wait.ForUnixContainer()
|
||||
.UntilHttpRequestIsSucceeded(request => request.ForPort(9333).ForPath("/cluster/healthz")))
|
||||
.Build();
|
||||
|
||||
public string ConnectionString => postgres.GetConnectionString();
|
||||
|
||||
public string S3ServiceUrl => $"http://{seaweedFs.Hostname}:{seaweedFs.GetMappedPublicPort(8333)}";
|
||||
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
public const string S3AccessKey = "musify-test";
|
||||
public const string S3SecretKey = "musify-test-secret";
|
||||
public const string S3Bucket = "musify-test-bucket";
|
||||
await Task.WhenAll(postgres.StartAsync(), seaweedFs.StartAsync());
|
||||
|
||||
private readonly PostgreSqlContainer postgres = new PostgreSqlBuilder("postgres:17-alpine")
|
||||
.WithDatabase("musify_test")
|
||||
.WithUsername("postgres")
|
||||
.WithPassword("postgres")
|
||||
.Build();
|
||||
await using var database = new Database(new DatabaseConfiguration { ConnectionString = ConnectionString });
|
||||
await database.Database.MigrateAsync();
|
||||
|
||||
private readonly IContainer seaweedFs = new ContainerBuilder("chrislusf/seaweedfs:latest")
|
||||
.WithEntrypoint("/bin/sh", "-c")
|
||||
.WithCommand(BuildSeaweedFsStartupScript())
|
||||
.WithPortBinding(9333, assignRandomHostPort: true)
|
||||
.WithPortBinding(8333, assignRandomHostPort: true)
|
||||
.WithWaitStrategy(Wait.ForUnixContainer()
|
||||
.UntilHttpRequestIsSucceeded(request => request.ForPort(9333).ForPath("/cluster/healthz")))
|
||||
.Build();
|
||||
|
||||
public string ConnectionString => postgres.GetConnectionString();
|
||||
|
||||
public string S3ServiceUrl => $"http://{seaweedFs.Hostname}:{seaweedFs.GetMappedPublicPort(8333)}";
|
||||
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
await Task.WhenAll(postgres.StartAsync(), seaweedFs.StartAsync());
|
||||
|
||||
await using var database = new Database(new DatabaseConfiguration { ConnectionString = ConnectionString });
|
||||
await database.Database.MigrateAsync();
|
||||
|
||||
using var s3 = CreateS3Client();
|
||||
await s3.PutBucketAsync(S3Bucket);
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await postgres.DisposeAsync();
|
||||
await seaweedFs.DisposeAsync();
|
||||
}
|
||||
|
||||
public Database CreateDatabase()
|
||||
{
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection([new("Database:ConnectionString", ConnectionString)])
|
||||
.Build();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddDatabase(configuration);
|
||||
return services.BuildServiceProvider().GetRequiredService<Database>();
|
||||
}
|
||||
|
||||
public IAmazonS3 CreateS3Client() =>
|
||||
new AmazonS3Client(S3AccessKey, S3SecretKey, new AmazonS3Config
|
||||
{
|
||||
ServiceURL = S3ServiceUrl,
|
||||
ForcePathStyle = true,
|
||||
UseHttp = true,
|
||||
});
|
||||
|
||||
private static string BuildSeaweedFsStartupScript()
|
||||
{
|
||||
var identity = JsonSerializer.Serialize(new
|
||||
{
|
||||
identities = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
name = "musify-test",
|
||||
credentials = new[] { new { accessKey = S3AccessKey, secretKey = S3SecretKey } },
|
||||
actions = new[] { "Admin", "Read", "Write" }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return $"""
|
||||
cat > /tmp/s3.conf <<'JSON'
|
||||
{identity}
|
||||
JSON
|
||||
exec weed server -ip.bind=0.0.0.0 -dir=/data -master.volumeSizeLimitMB=1024 -volume.max=0 -filer -s3 -s3.config=/tmp/s3.conf
|
||||
""";
|
||||
}
|
||||
using var s3 = CreateS3Client();
|
||||
await s3.PutBucketAsync(S3Bucket);
|
||||
}
|
||||
|
||||
[CollectionDefinition(Name)]
|
||||
public sealed class InfrastructureCollection : ICollectionFixture<InfrastructureTestFixture>
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
public const string Name = "Infrastructure";
|
||||
await postgres.DisposeAsync();
|
||||
await seaweedFs.DisposeAsync();
|
||||
}
|
||||
|
||||
public Database CreateDatabase()
|
||||
{
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection([new("Database:ConnectionString", ConnectionString)])
|
||||
.Build();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddDatabase(configuration);
|
||||
return services.BuildServiceProvider().GetRequiredService<Database>();
|
||||
}
|
||||
|
||||
public IAmazonS3 CreateS3Client() =>
|
||||
new AmazonS3Client(S3AccessKey, S3SecretKey, new AmazonS3Config
|
||||
{
|
||||
ServiceURL = S3ServiceUrl,
|
||||
ForcePathStyle = true,
|
||||
UseHttp = true,
|
||||
});
|
||||
|
||||
private static string BuildSeaweedFsStartupScript()
|
||||
{
|
||||
var identity = JsonSerializer.Serialize(new
|
||||
{
|
||||
identities = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
name = "musify-test",
|
||||
credentials = new[] { new { accessKey = S3AccessKey, secretKey = S3SecretKey } },
|
||||
actions = new[] { "Admin", "Read", "Write" }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return $"""
|
||||
cat > /tmp/s3.conf <<'JSON'
|
||||
{identity}
|
||||
JSON
|
||||
exec weed server -ip.bind=0.0.0.0 -dir=/data -master.volumeSizeLimitMB=1024 -volume.max=0 -filer -s3 -s3.config=/tmp/s3.conf
|
||||
""";
|
||||
}
|
||||
}
|
||||
|
||||
[CollectionDefinition(Name)]
|
||||
public sealed class InfrastructureCollection : ICollectionFixture<InfrastructureTestFixture>
|
||||
{
|
||||
public const string Name = "Infrastructure";
|
||||
}
|
||||
|
||||
+141
-142
@@ -6,160 +6,159 @@ using Musify.StreamingGateway.Authentication;
|
||||
using Musify.StreamingGateway.Configuration;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.StreamingGateway.Tests.Authentication
|
||||
namespace Musify.StreamingGateway.Tests.Authentication;
|
||||
|
||||
public sealed class TicketValidatorTests : IDisposable
|
||||
{
|
||||
public sealed class TicketValidatorTests : IDisposable
|
||||
private const string Issuer = "musify-webapi";
|
||||
private const string Audience = "media-gateway";
|
||||
|
||||
private readonly string publicKeyPath = Path.GetTempFileName();
|
||||
private readonly RSA rsa = RSA.Create(2048);
|
||||
private readonly JsonWebTokenHandler tokenHandler = new();
|
||||
|
||||
public TicketValidatorTests() => File.WriteAllText(publicKeyPath, rsa.ExportSubjectPublicKeyInfoPem());
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
private const string Issuer = "musify-webapi";
|
||||
private const string Audience = "media-gateway";
|
||||
rsa.Dispose();
|
||||
File.Delete(publicKeyPath);
|
||||
}
|
||||
|
||||
private readonly string publicKeyPath = Path.GetTempFileName();
|
||||
private readonly RSA rsa = RSA.Create(2048);
|
||||
private readonly JsonWebTokenHandler tokenHandler = new();
|
||||
private TicketValidator CreateValidator() => new(Options.Create(new StreamTicketValidationOptions
|
||||
{
|
||||
PublicKeyPath = publicKeyPath,
|
||||
Audience = Audience,
|
||||
Issuer = Issuer
|
||||
}));
|
||||
|
||||
public TicketValidatorTests() => File.WriteAllText(publicKeyPath, rsa.ExportSubjectPublicKeyInfoPem());
|
||||
private string IssueToken(string prefix = "Tracks/ProcessedAudios/abc/", string issuer = Issuer, string audience = Audience,
|
||||
DateTime? expires = null, DateTime? notBefore = null, long? maxBytes = null)
|
||||
{
|
||||
var claims = new Dictionary<string, object> { ["prefix"] = prefix };
|
||||
if (maxBytes != null)
|
||||
claims["maxBytes"] = maxBytes.Value.ToString();
|
||||
|
||||
public void Dispose()
|
||||
var descriptor = new SecurityTokenDescriptor
|
||||
{
|
||||
rsa.Dispose();
|
||||
File.Delete(publicKeyPath);
|
||||
}
|
||||
Issuer = issuer,
|
||||
Audience = audience,
|
||||
NotBefore = notBefore ?? DateTime.UtcNow.AddMinutes(-1),
|
||||
Expires = expires ?? DateTime.UtcNow.AddMinutes(5),
|
||||
Claims = claims,
|
||||
SigningCredentials = new SigningCredentials(new RsaSecurityKey(rsa), SecurityAlgorithms.RsaSha256),
|
||||
};
|
||||
return tokenHandler.CreateToken(descriptor);
|
||||
}
|
||||
|
||||
private TicketValidator CreateValidator() => new(Options.Create(new StreamTicketValidationOptions
|
||||
[Fact]
|
||||
public async Task ValidateTicketAsync_ValidToken_ReturnsThePrefixClaim()
|
||||
{
|
||||
var token = IssueToken(prefix: "Tracks/ProcessedAudios/xyz/");
|
||||
|
||||
var result = await CreateValidator().ValidateTicketAsync(token);
|
||||
|
||||
Assert.True(result.IsValid);
|
||||
Assert.Equal("Tracks/ProcessedAudios/xyz/", result.Prefix);
|
||||
Assert.Null(result.MaxBytes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateTicketAsync_TokenWithMaxBytesClaim_ReturnsIt()
|
||||
{
|
||||
var token = IssueToken(maxBytes: 480_000);
|
||||
|
||||
var result = await CreateValidator().ValidateTicketAsync(token);
|
||||
|
||||
Assert.True(result.IsValid);
|
||||
Assert.Equal(480_000, result.MaxBytes);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
public async Task ValidateTicketAsync_BlankToken_ReturnsInvalid(string? token)
|
||||
{
|
||||
var result = await CreateValidator().ValidateTicketAsync(token);
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Equal(string.Empty, result.Prefix);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateTicketAsync_MalformedToken_ReturnsInvalid()
|
||||
{
|
||||
var result = await CreateValidator().ValidateTicketAsync("not-a-real-jwt");
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateTicketAsync_ExpiredToken_ReturnsInvalid()
|
||||
{
|
||||
var token = IssueToken(expires: DateTime.UtcNow.AddMinutes(-40), notBefore: DateTime.UtcNow.AddMinutes(-50));
|
||||
|
||||
var result = await CreateValidator().ValidateTicketAsync(token);
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateTicketAsync_WrongIssuer_ReturnsInvalid()
|
||||
{
|
||||
var token = IssueToken(issuer: "someone-else");
|
||||
|
||||
var result = await CreateValidator().ValidateTicketAsync(token);
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateTicketAsync_WrongAudience_ReturnsInvalid()
|
||||
{
|
||||
var token = IssueToken(audience: "someone-else");
|
||||
|
||||
var result = await CreateValidator().ValidateTicketAsync(token);
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateTicketAsync_SignedWithADifferentKey_ReturnsInvalid()
|
||||
{
|
||||
using var otherKey = RSA.Create(2048);
|
||||
var descriptor = new SecurityTokenDescriptor
|
||||
{
|
||||
PublicKeyPath = publicKeyPath,
|
||||
Issuer = Issuer,
|
||||
Audience = Audience,
|
||||
Issuer = Issuer
|
||||
}));
|
||||
NotBefore = DateTime.UtcNow.AddMinutes(-1),
|
||||
Expires = DateTime.UtcNow.AddMinutes(5),
|
||||
Claims = new Dictionary<string, object> { ["prefix"] = "Tracks/ProcessedAudios/abc/" },
|
||||
SigningCredentials = new SigningCredentials(new RsaSecurityKey(otherKey), SecurityAlgorithms.RsaSha256),
|
||||
};
|
||||
var token = tokenHandler.CreateToken(descriptor);
|
||||
|
||||
private string IssueToken(string prefix = "Tracks/ProcessedAudios/abc/", string issuer = Issuer, string audience = Audience,
|
||||
DateTime? expires = null, DateTime? notBefore = null, long? maxBytes = null)
|
||||
var result = await CreateValidator().ValidateTicketAsync(token);
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateTicketAsync_MissingPrefixClaim_ReturnsInvalid()
|
||||
{
|
||||
var descriptor = new SecurityTokenDescriptor
|
||||
{
|
||||
var claims = new Dictionary<string, object> { ["prefix"] = prefix };
|
||||
if (maxBytes != null)
|
||||
claims["maxBytes"] = maxBytes.Value.ToString();
|
||||
Issuer = Issuer,
|
||||
Audience = Audience,
|
||||
NotBefore = DateTime.UtcNow.AddMinutes(-1),
|
||||
Expires = DateTime.UtcNow.AddMinutes(5),
|
||||
SigningCredentials = new SigningCredentials(new RsaSecurityKey(rsa), SecurityAlgorithms.RsaSha256),
|
||||
};
|
||||
var token = tokenHandler.CreateToken(descriptor);
|
||||
|
||||
var descriptor = new SecurityTokenDescriptor
|
||||
{
|
||||
Issuer = issuer,
|
||||
Audience = audience,
|
||||
NotBefore = notBefore ?? DateTime.UtcNow.AddMinutes(-1),
|
||||
Expires = expires ?? DateTime.UtcNow.AddMinutes(5),
|
||||
Claims = claims,
|
||||
SigningCredentials = new SigningCredentials(new RsaSecurityKey(rsa), SecurityAlgorithms.RsaSha256),
|
||||
};
|
||||
return tokenHandler.CreateToken(descriptor);
|
||||
}
|
||||
var result = await CreateValidator().ValidateTicketAsync(token);
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateTicketAsync_ValidToken_ReturnsThePrefixClaim()
|
||||
{
|
||||
var token = IssueToken(prefix: "Tracks/ProcessedAudios/xyz/");
|
||||
|
||||
var result = await CreateValidator().ValidateTicketAsync(token);
|
||||
|
||||
Assert.True(result.IsValid);
|
||||
Assert.Equal("Tracks/ProcessedAudios/xyz/", result.Prefix);
|
||||
Assert.Null(result.MaxBytes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateTicketAsync_TokenWithMaxBytesClaim_ReturnsIt()
|
||||
{
|
||||
var token = IssueToken(maxBytes: 480_000);
|
||||
|
||||
var result = await CreateValidator().ValidateTicketAsync(token);
|
||||
|
||||
Assert.True(result.IsValid);
|
||||
Assert.Equal(480_000, result.MaxBytes);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
public async Task ValidateTicketAsync_BlankToken_ReturnsInvalid(string? token)
|
||||
{
|
||||
var result = await CreateValidator().ValidateTicketAsync(token);
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Equal(string.Empty, result.Prefix);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateTicketAsync_MalformedToken_ReturnsInvalid()
|
||||
{
|
||||
var result = await CreateValidator().ValidateTicketAsync("not-a-real-jwt");
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateTicketAsync_ExpiredToken_ReturnsInvalid()
|
||||
{
|
||||
var token = IssueToken(expires: DateTime.UtcNow.AddMinutes(-40), notBefore: DateTime.UtcNow.AddMinutes(-50));
|
||||
|
||||
var result = await CreateValidator().ValidateTicketAsync(token);
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateTicketAsync_WrongIssuer_ReturnsInvalid()
|
||||
{
|
||||
var token = IssueToken(issuer: "someone-else");
|
||||
|
||||
var result = await CreateValidator().ValidateTicketAsync(token);
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateTicketAsync_WrongAudience_ReturnsInvalid()
|
||||
{
|
||||
var token = IssueToken(audience: "someone-else");
|
||||
|
||||
var result = await CreateValidator().ValidateTicketAsync(token);
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateTicketAsync_SignedWithADifferentKey_ReturnsInvalid()
|
||||
{
|
||||
using var otherKey = RSA.Create(2048);
|
||||
var descriptor = new SecurityTokenDescriptor
|
||||
{
|
||||
Issuer = Issuer,
|
||||
Audience = Audience,
|
||||
NotBefore = DateTime.UtcNow.AddMinutes(-1),
|
||||
Expires = DateTime.UtcNow.AddMinutes(5),
|
||||
Claims = new Dictionary<string, object> { ["prefix"] = "Tracks/ProcessedAudios/abc/" },
|
||||
SigningCredentials = new SigningCredentials(new RsaSecurityKey(otherKey), SecurityAlgorithms.RsaSha256),
|
||||
};
|
||||
var token = tokenHandler.CreateToken(descriptor);
|
||||
|
||||
var result = await CreateValidator().ValidateTicketAsync(token);
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateTicketAsync_MissingPrefixClaim_ReturnsInvalid()
|
||||
{
|
||||
var descriptor = new SecurityTokenDescriptor
|
||||
{
|
||||
Issuer = Issuer,
|
||||
Audience = Audience,
|
||||
NotBefore = DateTime.UtcNow.AddMinutes(-1),
|
||||
Expires = DateTime.UtcNow.AddMinutes(5),
|
||||
SigningCredentials = new SigningCredentials(new RsaSecurityKey(rsa), SecurityAlgorithms.RsaSha256),
|
||||
};
|
||||
var token = tokenHandler.CreateToken(descriptor);
|
||||
|
||||
var result = await CreateValidator().ValidateTicketAsync(token);
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
}
|
||||
Assert.False(result.IsValid);
|
||||
}
|
||||
}
|
||||
|
||||
+167
-168
@@ -9,212 +9,211 @@ using Musify.StreamingGateway.Configuration;
|
||||
using Musify.StreamingGateway.Middleware;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.StreamingGateway.Tests.Middleware
|
||||
namespace Musify.StreamingGateway.Tests.Middleware;
|
||||
|
||||
public sealed class TicketValidationMiddlewareTests : IDisposable
|
||||
{
|
||||
public sealed class TicketValidationMiddlewareTests : IDisposable
|
||||
private const string Prefix = "Tracks/ProcessedAudios/abc";
|
||||
|
||||
private readonly string publicKeyPath = Path.GetTempFileName();
|
||||
private readonly RSA rsa = RSA.Create(2048);
|
||||
private readonly StreamTicketValidationOptions options;
|
||||
|
||||
public TicketValidationMiddlewareTests()
|
||||
{
|
||||
private const string Prefix = "Tracks/ProcessedAudios/abc";
|
||||
|
||||
private readonly string publicKeyPath = Path.GetTempFileName();
|
||||
private readonly RSA rsa = RSA.Create(2048);
|
||||
private readonly StreamTicketValidationOptions options;
|
||||
|
||||
public TicketValidationMiddlewareTests()
|
||||
File.WriteAllText(publicKeyPath, rsa.ExportSubjectPublicKeyInfoPem());
|
||||
options = new StreamTicketValidationOptions
|
||||
{
|
||||
File.WriteAllText(publicKeyPath, rsa.ExportSubjectPublicKeyInfoPem());
|
||||
options = new StreamTicketValidationOptions
|
||||
{
|
||||
PublicKeyPath = publicKeyPath,
|
||||
Audience = "media-gateway",
|
||||
Issuer = "musify-webapi",
|
||||
};
|
||||
}
|
||||
PublicKeyPath = publicKeyPath,
|
||||
Audience = "media-gateway",
|
||||
Issuer = "musify-webapi",
|
||||
};
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
public void Dispose()
|
||||
{
|
||||
rsa.Dispose();
|
||||
File.Delete(publicKeyPath);
|
||||
}
|
||||
|
||||
private string IssueValidToken(string prefix = Prefix, long? maxBytes = null)
|
||||
{
|
||||
var claims = new Dictionary<string, object> { ["prefix"] = prefix };
|
||||
if (maxBytes != null)
|
||||
claims["maxBytes"] = maxBytes.Value.ToString();
|
||||
|
||||
return new JsonWebTokenHandler().CreateToken(new SecurityTokenDescriptor
|
||||
{
|
||||
rsa.Dispose();
|
||||
File.Delete(publicKeyPath);
|
||||
}
|
||||
Issuer = options.Issuer,
|
||||
Audience = options.Audience,
|
||||
NotBefore = DateTime.UtcNow.AddMinutes(-1),
|
||||
Expires = DateTime.UtcNow.AddMinutes(5),
|
||||
Claims = claims,
|
||||
SigningCredentials = new SigningCredentials(new RsaSecurityKey(rsa), SecurityAlgorithms.RsaSha256),
|
||||
});
|
||||
}
|
||||
|
||||
private string IssueValidToken(string prefix = Prefix, long? maxBytes = null)
|
||||
{
|
||||
var claims = new Dictionary<string, object> { ["prefix"] = prefix };
|
||||
if (maxBytes != null)
|
||||
claims["maxBytes"] = maxBytes.Value.ToString();
|
||||
private (TicketValidationMiddleware Middleware, Func<bool> WasNextCalled) CreateMiddleware()
|
||||
{
|
||||
var nextCalled = false;
|
||||
RequestDelegate next = _ => { nextCalled = true; return Task.CompletedTask; };
|
||||
var validator = new TicketValidator(Options.Create(options));
|
||||
var middleware = new TicketValidationMiddleware(next, validator, Options.Create(options), NullLogger<TicketValidationMiddleware>.Instance);
|
||||
return (middleware, () => nextCalled);
|
||||
}
|
||||
|
||||
return new JsonWebTokenHandler().CreateToken(new SecurityTokenDescriptor
|
||||
{
|
||||
Issuer = options.Issuer,
|
||||
Audience = options.Audience,
|
||||
NotBefore = DateTime.UtcNow.AddMinutes(-1),
|
||||
Expires = DateTime.UtcNow.AddMinutes(5),
|
||||
Claims = claims,
|
||||
SigningCredentials = new SigningCredentials(new RsaSecurityKey(rsa), SecurityAlgorithms.RsaSha256),
|
||||
});
|
||||
}
|
||||
[Fact]
|
||||
public async Task InvokeAsync_PathOutsideMediaPrefix_PassesThroughWithoutValidatingATicket()
|
||||
{
|
||||
var (middleware, wasNextCalled) = CreateMiddleware();
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Path = "/health";
|
||||
|
||||
private (TicketValidationMiddleware Middleware, Func<bool> WasNextCalled) CreateMiddleware()
|
||||
{
|
||||
var nextCalled = false;
|
||||
RequestDelegate next = _ => { nextCalled = true; return Task.CompletedTask; };
|
||||
var validator = new TicketValidator(Options.Create(options));
|
||||
var middleware = new TicketValidationMiddleware(next, validator, Options.Create(options), NullLogger<TicketValidationMiddleware>.Instance);
|
||||
return (middleware, () => nextCalled);
|
||||
}
|
||||
await middleware.InvokeAsync(context);
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAsync_PathOutsideMediaPrefix_PassesThroughWithoutValidatingATicket()
|
||||
{
|
||||
var (middleware, wasNextCalled) = CreateMiddleware();
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Path = "/health";
|
||||
Assert.True(wasNextCalled());
|
||||
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
|
||||
}
|
||||
|
||||
await middleware.InvokeAsync(context);
|
||||
[Fact]
|
||||
public async Task InvokeAsync_NoTicket_ReturnsUnauthorized()
|
||||
{
|
||||
var (middleware, wasNextCalled) = CreateMiddleware();
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Path = $"/media/{Prefix}/audio.m4a";
|
||||
|
||||
Assert.True(wasNextCalled());
|
||||
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
|
||||
}
|
||||
await middleware.InvokeAsync(context);
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAsync_NoTicket_ReturnsUnauthorized()
|
||||
{
|
||||
var (middleware, wasNextCalled) = CreateMiddleware();
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Path = $"/media/{Prefix}/audio.m4a";
|
||||
Assert.False(wasNextCalled());
|
||||
Assert.Equal(StatusCodes.Status401Unauthorized, context.Response.StatusCode);
|
||||
}
|
||||
|
||||
await middleware.InvokeAsync(context);
|
||||
[Fact]
|
||||
public async Task InvokeAsync_ValidTicketAsQueryParameter_ForTheRequestedObject_PassesThroughAndStripsTheTicket()
|
||||
{
|
||||
var (middleware, wasNextCalled) = CreateMiddleware();
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Path = $"/media/{Prefix}/audio.m4a";
|
||||
context.Request.QueryString = new QueryString($"?t={IssueValidToken()}&keep=1");
|
||||
|
||||
Assert.False(wasNextCalled());
|
||||
Assert.Equal(StatusCodes.Status401Unauthorized, context.Response.StatusCode);
|
||||
}
|
||||
await middleware.InvokeAsync(context);
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAsync_ValidTicketAsQueryParameter_ForTheRequestedObject_PassesThroughAndStripsTheTicket()
|
||||
{
|
||||
var (middleware, wasNextCalled) = CreateMiddleware();
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Path = $"/media/{Prefix}/audio.m4a";
|
||||
context.Request.QueryString = new QueryString($"?t={IssueValidToken()}&keep=1");
|
||||
Assert.True(wasNextCalled());
|
||||
Assert.Equal("keep=1", context.Request.QueryString.Value?.TrimStart('?'));
|
||||
}
|
||||
|
||||
await middleware.InvokeAsync(context);
|
||||
[Fact]
|
||||
public async Task InvokeAsync_ValidTicketAsHeader_PassesThrough()
|
||||
{
|
||||
var (middleware, wasNextCalled) = CreateMiddleware();
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Path = $"/media/{Prefix}/audio.m4a";
|
||||
context.Request.Headers["X-Stream-Ticket"] = IssueValidToken();
|
||||
|
||||
Assert.True(wasNextCalled());
|
||||
Assert.Equal("keep=1", context.Request.QueryString.Value?.TrimStart('?'));
|
||||
}
|
||||
await middleware.InvokeAsync(context);
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAsync_ValidTicketAsHeader_PassesThrough()
|
||||
{
|
||||
var (middleware, wasNextCalled) = CreateMiddleware();
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Path = $"/media/{Prefix}/audio.m4a";
|
||||
context.Request.Headers["X-Stream-Ticket"] = IssueValidToken();
|
||||
Assert.True(wasNextCalled());
|
||||
}
|
||||
|
||||
await middleware.InvokeAsync(context);
|
||||
[Fact]
|
||||
public async Task InvokeAsync_ObjectKeyOutsideAuthorizedPrefix_ReturnsForbidden()
|
||||
{
|
||||
var (middleware, wasNextCalled) = CreateMiddleware();
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Path = "/media/Tracks/ProcessedAudios/someone-elses-track/audio.m4a";
|
||||
context.Request.QueryString = new QueryString($"?t={IssueValidToken()}");
|
||||
|
||||
Assert.True(wasNextCalled());
|
||||
}
|
||||
await middleware.InvokeAsync(context);
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAsync_ObjectKeyOutsideAuthorizedPrefix_ReturnsForbidden()
|
||||
{
|
||||
var (middleware, wasNextCalled) = CreateMiddleware();
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Path = "/media/Tracks/ProcessedAudios/someone-elses-track/audio.m4a";
|
||||
context.Request.QueryString = new QueryString($"?t={IssueValidToken()}");
|
||||
Assert.False(wasNextCalled());
|
||||
Assert.Equal(StatusCodes.Status403Forbidden, context.Response.StatusCode);
|
||||
}
|
||||
|
||||
await middleware.InvokeAsync(context);
|
||||
[Theory]
|
||||
[InlineData($"/media/{Prefix}/../../etc/passwd")]
|
||||
[InlineData($"/media/{Prefix}/./audio.m4a")]
|
||||
public async Task InvokeAsync_PathTraversalSegment_ReturnsBadRequest(string path)
|
||||
{
|
||||
var (middleware, wasNextCalled) = CreateMiddleware();
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Path = path;
|
||||
context.Request.QueryString = new QueryString($"?t={IssueValidToken()}");
|
||||
|
||||
Assert.False(wasNextCalled());
|
||||
Assert.Equal(StatusCodes.Status403Forbidden, context.Response.StatusCode);
|
||||
}
|
||||
await middleware.InvokeAsync(context);
|
||||
|
||||
[Theory]
|
||||
[InlineData($"/media/{Prefix}/../../etc/passwd")]
|
||||
[InlineData($"/media/{Prefix}/./audio.m4a")]
|
||||
public async Task InvokeAsync_PathTraversalSegment_ReturnsBadRequest(string path)
|
||||
{
|
||||
var (middleware, wasNextCalled) = CreateMiddleware();
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Path = path;
|
||||
context.Request.QueryString = new QueryString($"?t={IssueValidToken()}");
|
||||
Assert.False(wasNextCalled());
|
||||
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
|
||||
}
|
||||
|
||||
await middleware.InvokeAsync(context);
|
||||
[Fact]
|
||||
public async Task InvokeAsync_TicketPrefixExactlyMatchesTheRequestedObject_PassesThrough()
|
||||
{
|
||||
var (middleware, wasNextCalled) = CreateMiddleware();
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Path = $"/media/{Prefix}";
|
||||
context.Request.QueryString = new QueryString($"?t={IssueValidToken()}");
|
||||
|
||||
Assert.False(wasNextCalled());
|
||||
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
|
||||
}
|
||||
await middleware.InvokeAsync(context);
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAsync_TicketPrefixExactlyMatchesTheRequestedObject_PassesThrough()
|
||||
{
|
||||
var (middleware, wasNextCalled) = CreateMiddleware();
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Path = $"/media/{Prefix}";
|
||||
context.Request.QueryString = new QueryString($"?t={IssueValidToken()}");
|
||||
Assert.True(wasNextCalled());
|
||||
}
|
||||
|
||||
await middleware.InvokeAsync(context);
|
||||
[Fact]
|
||||
public async Task InvokeAsync_TicketWithMaxBytes_NoRangeRequested_SynthesizesABoundedRange()
|
||||
{
|
||||
var (middleware, wasNextCalled) = CreateMiddleware();
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Path = $"/media/{Prefix}/audio.m4a";
|
||||
context.Request.QueryString = new QueryString($"?t={IssueValidToken(maxBytes: 480_000)}");
|
||||
|
||||
Assert.True(wasNextCalled());
|
||||
}
|
||||
await middleware.InvokeAsync(context);
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAsync_TicketWithMaxBytes_NoRangeRequested_SynthesizesABoundedRange()
|
||||
{
|
||||
var (middleware, wasNextCalled) = CreateMiddleware();
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Path = $"/media/{Prefix}/audio.m4a";
|
||||
context.Request.QueryString = new QueryString($"?t={IssueValidToken(maxBytes: 480_000)}");
|
||||
Assert.True(wasNextCalled());
|
||||
Assert.Equal("bytes=0-479999", context.Request.Headers.Range.ToString());
|
||||
}
|
||||
|
||||
await middleware.InvokeAsync(context);
|
||||
[Fact]
|
||||
public async Task InvokeAsync_TicketWithMaxBytes_RangeWithinCap_ClampsTheEndOnly()
|
||||
{
|
||||
var (middleware, wasNextCalled) = CreateMiddleware();
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Path = $"/media/{Prefix}/audio.m4a";
|
||||
context.Request.QueryString = new QueryString($"?t={IssueValidToken(maxBytes: 480_000)}");
|
||||
context.Request.Headers.Range = "bytes=1000-";
|
||||
|
||||
Assert.True(wasNextCalled());
|
||||
Assert.Equal("bytes=0-479999", context.Request.Headers.Range.ToString());
|
||||
}
|
||||
await middleware.InvokeAsync(context);
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAsync_TicketWithMaxBytes_RangeWithinCap_ClampsTheEndOnly()
|
||||
{
|
||||
var (middleware, wasNextCalled) = CreateMiddleware();
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Path = $"/media/{Prefix}/audio.m4a";
|
||||
context.Request.QueryString = new QueryString($"?t={IssueValidToken(maxBytes: 480_000)}");
|
||||
context.Request.Headers.Range = "bytes=1000-";
|
||||
Assert.True(wasNextCalled());
|
||||
Assert.Equal("bytes=1000-479999", context.Request.Headers.Range.ToString());
|
||||
}
|
||||
|
||||
await middleware.InvokeAsync(context);
|
||||
[Fact]
|
||||
public async Task InvokeAsync_TicketWithMaxBytes_RangeStartsAtOrPastTheCap_ReturnsRangeNotSatisfiable()
|
||||
{
|
||||
var (middleware, wasNextCalled) = CreateMiddleware();
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Path = $"/media/{Prefix}/audio.m4a";
|
||||
context.Request.QueryString = new QueryString($"?t={IssueValidToken(maxBytes: 480_000)}");
|
||||
context.Request.Headers.Range = "bytes=480000-";
|
||||
|
||||
Assert.True(wasNextCalled());
|
||||
Assert.Equal("bytes=1000-479999", context.Request.Headers.Range.ToString());
|
||||
}
|
||||
await middleware.InvokeAsync(context);
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAsync_TicketWithMaxBytes_RangeStartsAtOrPastTheCap_ReturnsRangeNotSatisfiable()
|
||||
{
|
||||
var (middleware, wasNextCalled) = CreateMiddleware();
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Path = $"/media/{Prefix}/audio.m4a";
|
||||
context.Request.QueryString = new QueryString($"?t={IssueValidToken(maxBytes: 480_000)}");
|
||||
context.Request.Headers.Range = "bytes=480000-";
|
||||
Assert.False(wasNextCalled());
|
||||
Assert.Equal(StatusCodes.Status416RangeNotSatisfiable, context.Response.StatusCode);
|
||||
}
|
||||
|
||||
await middleware.InvokeAsync(context);
|
||||
[Fact]
|
||||
public async Task InvokeAsync_TicketWithoutMaxBytes_LeavesTheRequestedRangeUntouched()
|
||||
{
|
||||
var (middleware, wasNextCalled) = CreateMiddleware();
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Path = $"/media/{Prefix}/audio.m4a";
|
||||
context.Request.QueryString = new QueryString($"?t={IssueValidToken()}");
|
||||
context.Request.Headers.Range = "bytes=1000-2000";
|
||||
|
||||
Assert.False(wasNextCalled());
|
||||
Assert.Equal(StatusCodes.Status416RangeNotSatisfiable, context.Response.StatusCode);
|
||||
}
|
||||
await middleware.InvokeAsync(context);
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAsync_TicketWithoutMaxBytes_LeavesTheRequestedRangeUntouched()
|
||||
{
|
||||
var (middleware, wasNextCalled) = CreateMiddleware();
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Path = $"/media/{Prefix}/audio.m4a";
|
||||
context.Request.QueryString = new QueryString($"?t={IssueValidToken()}");
|
||||
context.Request.Headers.Range = "bytes=1000-2000";
|
||||
|
||||
await middleware.InvokeAsync(context);
|
||||
|
||||
Assert.True(wasNextCalled());
|
||||
Assert.Equal("bytes=1000-2000", context.Request.Headers.Range.ToString());
|
||||
}
|
||||
Assert.True(wasNextCalled());
|
||||
Assert.Equal("bytes=1000-2000", context.Request.Headers.Range.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user