Change editor rule
This commit is contained in:
@@ -3,64 +3,65 @@ using Microsoft.AspNetCore.Http;
|
||||
using Musify.Api.Authentication;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Api.Tests.Authentication;
|
||||
|
||||
public sealed class CurrentUserTests
|
||||
namespace Musify.Api.Tests.Authentication
|
||||
{
|
||||
private static async Task<CurrentUser> BindAsync(ClaimsPrincipal principal)
|
||||
public sealed class CurrentUserTests
|
||||
{
|
||||
var context = new DefaultHttpContext { User = principal };
|
||||
var bound = await CurrentUser.BindAsync(context, parameter: null!);
|
||||
return bound!;
|
||||
}
|
||||
private static async Task<CurrentUser> BindAsync(ClaimsPrincipal principal)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,121 +9,122 @@ using Musify.Application.Pictures.Responses;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Api.Tests.Endpoints;
|
||||
|
||||
[Collection(ApiCollection.Name)]
|
||||
public sealed class AlbumEndpointsTests(ApiTestFixture fixture)
|
||||
namespace Musify.Api.Tests.Endpoints
|
||||
{
|
||||
private Task<long> CreateUserAsync() => fixture.SeedUserAsync();
|
||||
|
||||
private async Task<Guid> CreatePictureIntentAsync(HttpClient client)
|
||||
[Collection(ApiCollection.Name)]
|
||||
public sealed class AlbumEndpointsTests(ApiTestFixture fixture)
|
||||
{
|
||||
fixture.StorageService
|
||||
.GetUploadUrlAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<TimeSpan>(), Arg.Any<bool>(), 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 Task<long> CreateUserAsync() => fixture.SeedUserAsync();
|
||||
|
||||
var response = await client.PostAsJsonAsync(
|
||||
"/albums/upload-picture",
|
||||
new PictureUploadRequest("webp", "image/webp"),
|
||||
TestContext.Current.CancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
private async Task<Guid> CreatePictureIntentAsync(HttpClient client)
|
||||
{
|
||||
fixture.StorageService
|
||||
.GetUploadUrlAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<TimeSpan>(), Arg.Any<bool>(), 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 body = await response.Content.ReadFromJsonAsync<PictureUploadResponse>(TestContext.Current.CancellationToken);
|
||||
return body!.IntentId;
|
||||
}
|
||||
var response = await client.PostAsJsonAsync(
|
||||
"/albums/upload-picture",
|
||||
new PictureUploadRequest("webp", "image/webp"),
|
||||
TestContext.Current.CancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
[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);
|
||||
var body = await response.Content.ReadFromJsonAsync<PictureUploadResponse>(TestContext.Current.CancellationToken);
|
||||
return body!.IntentId;
|
||||
}
|
||||
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||
}
|
||||
[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_AuthenticatedExistingUser_ReturnsCreatedAlbum()
|
||||
{
|
||||
var userId = await CreateUserAsync();
|
||||
var client = fixture.CreateAuthenticatedClient(userId);
|
||||
var pictureIntentId = await CreatePictureIntentAsync(client);
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||
}
|
||||
|
||||
var response = await client.PostAsJsonAsync("/albums", new CreateAlbumRequest("My Album", "desc", 2024, pictureIntentId), TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task PostAlbums_AuthenticatedExistingUser_ReturnsCreatedAlbum()
|
||||
{
|
||||
var userId = await CreateUserAsync();
|
||||
var client = fixture.CreateAuthenticatedClient(userId);
|
||||
var pictureIntentId = await CreatePictureIntentAsync(client);
|
||||
|
||||
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);
|
||||
}
|
||||
var response = await client.PostAsJsonAsync("/albums", new CreateAlbumRequest("My Album", "desc", 2024, pictureIntentId), TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task PostAlbums_BlankTitle_ReturnsValidationProblem()
|
||||
{
|
||||
var userId = await CreateUserAsync();
|
||||
var client = fixture.CreateAuthenticatedClient(userId);
|
||||
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);
|
||||
}
|
||||
|
||||
var response = await client.PostAsJsonAsync("/albums", new CreateAlbumRequest("", null, null, Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task PostAlbums_BlankTitle_ReturnsValidationProblem()
|
||||
{
|
||||
var userId = await CreateUserAsync();
|
||||
var client = fixture.CreateAuthenticatedClient(userId);
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
var response = await client.PostAsJsonAsync("/albums", new CreateAlbumRequest("", null, null, Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task PostAlbums_MissingPictureIntent_ReturnsValidationProblem()
|
||||
{
|
||||
var userId = await CreateUserAsync();
|
||||
var client = fixture.CreateAuthenticatedClient(userId);
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
|
||||
var response = await client.PostAsJsonAsync("/albums", new CreateAlbumRequest("No Picture", null, null, Guid.Empty), TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task PostAlbums_MissingPictureIntent_ReturnsValidationProblem()
|
||||
{
|
||||
var userId = await CreateUserAsync();
|
||||
var client = fixture.CreateAuthenticatedClient(userId);
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
var response = await client.PostAsJsonAsync("/albums", new CreateAlbumRequest("No Picture", null, null, Guid.Empty), 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);
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
|
||||
var response = await fixture.CreateAnonymousClient().GetAsync($"/albums/{created!.Id}", 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);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
}
|
||||
var response = await fixture.CreateAnonymousClient().GetAsync($"/albums/{created!.Id}", TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteAlbum_NotTheOwner_ReturnsForbidden()
|
||||
{
|
||||
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);
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
}
|
||||
|
||||
var response = await fixture.CreateAuthenticatedClient(stranger).DeleteAsync($"/albums/{created!.Id}", TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task DeleteAlbum_NotTheOwner_ReturnsForbidden()
|
||||
{
|
||||
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);
|
||||
|
||||
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
|
||||
}
|
||||
var response = await fixture.CreateAuthenticatedClient(stranger).DeleteAsync($"/albums/{created!.Id}", 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);
|
||||
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
|
||||
}
|
||||
|
||||
var deleteResponse = await client.DeleteAsync($"/albums/{created!.Id}", TestContext.Current.CancellationToken);
|
||||
var getResponse = await fixture.CreateAnonymousClient().GetAsync($"/albums/{created.Id}", 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);
|
||||
|
||||
Assert.Equal(HttpStatusCode.NoContent, deleteResponse.StatusCode);
|
||||
Assert.Equal(HttpStatusCode.NotFound, getResponse.StatusCode);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,21 +4,22 @@ using Musify.Api.Tests.TestSupport;
|
||||
using Musify.Application.Configuration.Responses;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Api.Tests.Endpoints;
|
||||
|
||||
[Collection(ApiCollection.Name)]
|
||||
public sealed class ConfigEndpointsTests(ApiTestFixture fixture)
|
||||
namespace Musify.Api.Tests.Endpoints
|
||||
{
|
||||
[Fact]
|
||||
public async Task GetPlaybackConfig_WithoutAuthentication_ReturnsConfiguredValues()
|
||||
[Collection(ApiCollection.Name)]
|
||||
public sealed class ConfigEndpointsTests(ApiTestFixture fixture)
|
||||
{
|
||||
var client = fixture.CreateAnonymousClient();
|
||||
[Fact]
|
||||
public async Task GetPlaybackConfig_WithoutAuthentication_ReturnsConfiguredValues()
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,94 +12,95 @@ using Musify.Domain.ValueObjects;
|
||||
using Musify.Infrastructure.Persistence;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Api.Tests.Endpoints;
|
||||
|
||||
[Collection(ApiCollection.Name)]
|
||||
public sealed class GenreEndpointsTests(ApiTestFixture fixture)
|
||||
namespace Musify.Api.Tests.Endpoints
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
[Collection(ApiCollection.Name)]
|
||||
public sealed class GenreEndpointsTests(ApiTestFixture fixture)
|
||||
{
|
||||
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
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
Title = title,
|
||||
NormalizedTitle = title.ToUpperInvariant(),
|
||||
OwnerUserId = owner.Id,
|
||||
Owner = owner,
|
||||
Tags = [new TrackTag { TrackId = Guid.Empty, Tag = genre }]
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
};
|
||||
foreach (var tag in track.Tags)
|
||||
tag.TrackId = track.Id;
|
||||
database.Users.Add(owner);
|
||||
database.Tracks.Add(track);
|
||||
await database.SaveChangesAsync(TestContext.Current.CancellationToken);
|
||||
return title;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetGenres_Anonymous_ListsGenresInUse()
|
||||
{
|
||||
await SeedTrackAsync(Genre.Funk);
|
||||
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);
|
||||
await database.SaveChangesAsync(TestContext.Current.CancellationToken);
|
||||
return title;
|
||||
}
|
||||
|
||||
var response = await fixture.CreateAnonymousClient().GetAsync("/genres", TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task GetGenres_Anonymous_ListsGenresInUse()
|
||||
{
|
||||
await SeedTrackAsync(Genre.Funk);
|
||||
|
||||
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);
|
||||
}
|
||||
var response = await fixture.CreateAnonymousClient().GetAsync("/genres", 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<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<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);
|
||||
}
|
||||
[Fact]
|
||||
public async Task GetAvailableGenres_Anonymous_ListsEveryGenreWithIncompatibilities()
|
||||
{
|
||||
var response = await fixture.CreateAnonymousClient().GetAsync("/genres/available", 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.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.BadRequest, response.StatusCode);
|
||||
}
|
||||
[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);
|
||||
|
||||
[Fact]
|
||||
public async Task GetTracks_GenreQuery_IsCaseInsensitive()
|
||||
{
|
||||
var title = await SeedTrackAsync(Genre.Blues);
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
|
||||
var response = await fixture.CreateAnonymousClient().GetAsync("/tracks?genre=blues&pageSize=100", TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task GetTracks_GenreQuery_IsCaseInsensitive()
|
||||
{
|
||||
var title = await SeedTrackAsync(Genre.Blues);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var body = await response.Content.ReadFromJsonAsync<PaginatedResponse<TrackApplicationResponse>>(JsonOptions, TestContext.Current.CancellationToken);
|
||||
Assert.Contains(body!.Items, item => item.Title == title);
|
||||
}
|
||||
var response = await fixture.CreateAnonymousClient().GetAsync("/tracks?genre=blues&pageSize=100", TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task GetTracks_GenreQuery_FiltersByTag()
|
||||
{
|
||||
var title = await SeedTrackAsync(Genre.Ambient);
|
||||
await SeedTrackAsync(Genre.Techno);
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var body = await response.Content.ReadFromJsonAsync<PaginatedResponse<TrackApplicationResponse>>(JsonOptions, TestContext.Current.CancellationToken);
|
||||
Assert.Contains(body!.Items, item => item.Title == title);
|
||||
}
|
||||
|
||||
var response = await fixture.CreateAnonymousClient().GetAsync("/tracks?genre=Ambient&pageSize=100", TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task GetTracks_GenreQuery_FiltersByTag()
|
||||
{
|
||||
var title = await SeedTrackAsync(Genre.Ambient);
|
||||
await SeedTrackAsync(Genre.Techno);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var body = await response.Content.ReadFromJsonAsync<PaginatedResponse<TrackApplicationResponse>>(JsonOptions, TestContext.Current.CancellationToken);
|
||||
Assert.Contains(body!.Items, item => item.Title == title);
|
||||
Assert.All(body.Items, item => Assert.Contains(Genre.Ambient, item.Tags));
|
||||
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<PaginatedResponse<TrackApplicationResponse>>(JsonOptions, TestContext.Current.CancellationToken);
|
||||
Assert.Contains(body!.Items, item => item.Title == title);
|
||||
Assert.All(body.Items, item => Assert.Contains(Genre.Ambient, item.Tags));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,93 +8,94 @@ using Musify.Domain.Entities;
|
||||
using Musify.Infrastructure.Persistence;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Api.Tests.Endpoints;
|
||||
|
||||
[Collection(ApiCollection.Name)]
|
||||
public sealed class ListeningProgressEndpointsTests(ApiTestFixture fixture)
|
||||
namespace Musify.Api.Tests.Endpoints
|
||||
{
|
||||
private Task<long> CreateUserAsync() => fixture.SeedUserAsync();
|
||||
|
||||
private async Task<Guid> SeedListenAsync(long userId, double durationSeconds)
|
||||
[Collection(ApiCollection.Name)]
|
||||
public sealed class ListeningProgressEndpointsTests(ApiTestFixture fixture)
|
||||
{
|
||||
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
|
||||
private Task<long> CreateUserAsync() => fixture.SeedUserAsync();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private async Task<ListeningHistory> LoadListenAsync(Guid listenId)
|
||||
{
|
||||
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;
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,148 +9,149 @@ using Musify.Application.Shared;
|
||||
using Musify.Domain.ValueObjects;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Api.Tests.Endpoints;
|
||||
|
||||
[Collection(ApiCollection.Name)]
|
||||
public sealed class PlayListEndpointsTests(ApiTestFixture fixture)
|
||||
namespace Musify.Api.Tests.Endpoints
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
[Collection(ApiCollection.Name)]
|
||||
public sealed class PlayListEndpointsTests(ApiTestFixture fixture)
|
||||
{
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
};
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
};
|
||||
|
||||
private static CancellationToken Ct => TestContext.Current.CancellationToken;
|
||||
private static CancellationToken Ct => TestContext.Current.CancellationToken;
|
||||
|
||||
private async Task<PlayListApplicationResponse> CreatePlayListAsync(long ownerId, PlayListVisibility visibility, string? name = null)
|
||||
{
|
||||
var response = await fixture.CreateAuthenticatedClient(ownerId).PostAsJsonAsync(
|
||||
"/playlists",
|
||||
new CreatePlayListRequest(name ?? $"pl-{Guid.NewGuid():N}"[..20], null, null, visibility),
|
||||
Ct);
|
||||
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
|
||||
return (await response.Content.ReadFromJsonAsync<PlayListApplicationResponse>(JsonOptions, Ct))!;
|
||||
}
|
||||
private async Task<PlayListApplicationResponse> CreatePlayListAsync(long ownerId, PlayListVisibility visibility, string? name = null)
|
||||
{
|
||||
var response = await fixture.CreateAuthenticatedClient(ownerId).PostAsJsonAsync(
|
||||
"/playlists",
|
||||
new CreatePlayListRequest(name ?? $"pl-{Guid.NewGuid():N}"[..20], null, null, visibility),
|
||||
Ct);
|
||||
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
|
||||
return (await response.Content.ReadFromJsonAsync<PlayListApplicationResponse>(JsonOptions, Ct))!;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetPlayListById_PrivatePlayList_IsOnlyVisibleToItsOwner()
|
||||
{
|
||||
var owner = await fixture.SeedUserAsync();
|
||||
var stranger = await fixture.SeedUserAsync();
|
||||
var playList = await CreatePlayListAsync(owner, PlayListVisibility.Private);
|
||||
[Fact]
|
||||
public async Task GetPlayListById_PrivatePlayList_IsOnlyVisibleToItsOwner()
|
||||
{
|
||||
var owner = await fixture.SeedUserAsync();
|
||||
var stranger = await fixture.SeedUserAsync();
|
||||
var playList = await CreatePlayListAsync(owner, PlayListVisibility.Private);
|
||||
|
||||
var asOwner = await fixture.CreateAuthenticatedClient(owner).GetAsync($"/playlists/{playList.Id}", Ct);
|
||||
var asStranger = await fixture.CreateAuthenticatedClient(stranger).GetAsync($"/playlists/{playList.Id}", Ct);
|
||||
var asAnonymous = await fixture.CreateAnonymousClient().GetAsync($"/playlists/{playList.Id}", Ct);
|
||||
var asOwner = await fixture.CreateAuthenticatedClient(owner).GetAsync($"/playlists/{playList.Id}", Ct);
|
||||
var asStranger = await fixture.CreateAuthenticatedClient(stranger).GetAsync($"/playlists/{playList.Id}", Ct);
|
||||
var asAnonymous = await fixture.CreateAnonymousClient().GetAsync($"/playlists/{playList.Id}", Ct);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, asOwner.StatusCode);
|
||||
Assert.Equal(HttpStatusCode.NotFound, asStranger.StatusCode);
|
||||
Assert.Equal(HttpStatusCode.NotFound, asAnonymous.StatusCode);
|
||||
}
|
||||
Assert.Equal(HttpStatusCode.OK, asOwner.StatusCode);
|
||||
Assert.Equal(HttpStatusCode.NotFound, asStranger.StatusCode);
|
||||
Assert.Equal(HttpStatusCode.NotFound, asAnonymous.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetPlayListById_PublicPlayList_IsVisibleToAnonymousUsers()
|
||||
{
|
||||
var owner = await fixture.SeedUserAsync();
|
||||
var playList = await CreatePlayListAsync(owner, PlayListVisibility.Public);
|
||||
[Fact]
|
||||
public async Task GetPlayListById_PublicPlayList_IsVisibleToAnonymousUsers()
|
||||
{
|
||||
var owner = await fixture.SeedUserAsync();
|
||||
var playList = await CreatePlayListAsync(owner, PlayListVisibility.Public);
|
||||
|
||||
var response = await fixture.CreateAnonymousClient().GetAsync($"/playlists/{playList.Id}", Ct);
|
||||
var response = await fixture.CreateAnonymousClient().GetAsync($"/playlists/{playList.Id}", Ct);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
}
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetPlayListTracks_PrivatePlayList_IsHiddenFromStrangers()
|
||||
{
|
||||
var owner = await fixture.SeedUserAsync();
|
||||
var stranger = await fixture.SeedUserAsync();
|
||||
var playList = await CreatePlayListAsync(owner, PlayListVisibility.Private);
|
||||
[Fact]
|
||||
public async Task GetPlayListTracks_PrivatePlayList_IsHiddenFromStrangers()
|
||||
{
|
||||
var owner = await fixture.SeedUserAsync();
|
||||
var stranger = await fixture.SeedUserAsync();
|
||||
var playList = await CreatePlayListAsync(owner, PlayListVisibility.Private);
|
||||
|
||||
var asOwner = await fixture.CreateAuthenticatedClient(owner).GetAsync($"/playlists/{playList.Id}/tracks", Ct);
|
||||
var asStranger = await fixture.CreateAuthenticatedClient(stranger).GetAsync($"/playlists/{playList.Id}/tracks", Ct);
|
||||
var asOwner = await fixture.CreateAuthenticatedClient(owner).GetAsync($"/playlists/{playList.Id}/tracks", Ct);
|
||||
var asStranger = await fixture.CreateAuthenticatedClient(stranger).GetAsync($"/playlists/{playList.Id}/tracks", Ct);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, asOwner.StatusCode);
|
||||
Assert.Equal(HttpStatusCode.NotFound, asStranger.StatusCode);
|
||||
}
|
||||
Assert.Equal(HttpStatusCode.OK, asOwner.StatusCode);
|
||||
Assert.Equal(HttpStatusCode.NotFound, asStranger.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetPlayListsByUserId_StrangersOnlySeePublicPlayLists()
|
||||
{
|
||||
var owner = await fixture.SeedUserAsync();
|
||||
var stranger = await fixture.SeedUserAsync();
|
||||
var publicPlayList = await CreatePlayListAsync(owner, PlayListVisibility.Public);
|
||||
var privatePlayList = await CreatePlayListAsync(owner, PlayListVisibility.Private);
|
||||
[Fact]
|
||||
public async Task GetPlayListsByUserId_StrangersOnlySeePublicPlayLists()
|
||||
{
|
||||
var owner = await fixture.SeedUserAsync();
|
||||
var stranger = await fixture.SeedUserAsync();
|
||||
var publicPlayList = await CreatePlayListAsync(owner, PlayListVisibility.Public);
|
||||
var privatePlayList = await CreatePlayListAsync(owner, PlayListVisibility.Private);
|
||||
|
||||
var asStranger = await ReadPlayListIdsAsync(fixture.CreateAuthenticatedClient(stranger), owner);
|
||||
var asOwner = await ReadPlayListIdsAsync(fixture.CreateAuthenticatedClient(owner), owner);
|
||||
var asStranger = await ReadPlayListIdsAsync(fixture.CreateAuthenticatedClient(stranger), owner);
|
||||
var asOwner = await ReadPlayListIdsAsync(fixture.CreateAuthenticatedClient(owner), owner);
|
||||
|
||||
Assert.Equal([publicPlayList.Id], asStranger);
|
||||
Assert.Contains(publicPlayList.Id, asOwner);
|
||||
Assert.Contains(privatePlayList.Id, asOwner);
|
||||
}
|
||||
Assert.Equal([publicPlayList.Id], asStranger);
|
||||
Assert.Contains(publicPlayList.Id, asOwner);
|
||||
Assert.Contains(privatePlayList.Id, asOwner);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdatePlayList_NotTheOwner_ReturnsForbidden()
|
||||
{
|
||||
var owner = await fixture.SeedUserAsync();
|
||||
var stranger = await fixture.SeedUserAsync();
|
||||
var playList = await CreatePlayListAsync(owner, PlayListVisibility.Public);
|
||||
[Fact]
|
||||
public async Task UpdatePlayList_NotTheOwner_ReturnsForbidden()
|
||||
{
|
||||
var owner = await fixture.SeedUserAsync();
|
||||
var stranger = await fixture.SeedUserAsync();
|
||||
var playList = await CreatePlayListAsync(owner, PlayListVisibility.Public);
|
||||
|
||||
var response = await fixture.CreateAuthenticatedClient(stranger).PutAsJsonAsync(
|
||||
$"/playlists/{playList.Id}", new UpdatePlayListRequest("Hijacked", null, null), Ct);
|
||||
var response = await fixture.CreateAuthenticatedClient(stranger).PutAsJsonAsync(
|
||||
$"/playlists/{playList.Id}", new UpdatePlayListRequest("Hijacked", null, null), Ct);
|
||||
|
||||
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
|
||||
}
|
||||
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeletePlayList_NotTheOwner_ReturnsForbidden()
|
||||
{
|
||||
var owner = await fixture.SeedUserAsync();
|
||||
var stranger = await fixture.SeedUserAsync();
|
||||
var playList = await CreatePlayListAsync(owner, PlayListVisibility.Public);
|
||||
[Fact]
|
||||
public async Task DeletePlayList_NotTheOwner_ReturnsForbidden()
|
||||
{
|
||||
var owner = await fixture.SeedUserAsync();
|
||||
var stranger = await fixture.SeedUserAsync();
|
||||
var playList = await CreatePlayListAsync(owner, PlayListVisibility.Public);
|
||||
|
||||
var response = await fixture.CreateAuthenticatedClient(stranger).DeleteAsync($"/playlists/{playList.Id}", Ct);
|
||||
var response = await fixture.CreateAuthenticatedClient(stranger).DeleteAsync($"/playlists/{playList.Id}", Ct);
|
||||
|
||||
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
|
||||
}
|
||||
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdatePlayList_Owner_RenamesIt()
|
||||
{
|
||||
var owner = await fixture.SeedUserAsync();
|
||||
var playList = await CreatePlayListAsync(owner, PlayListVisibility.Private);
|
||||
[Fact]
|
||||
public async Task UpdatePlayList_Owner_RenamesIt()
|
||||
{
|
||||
var owner = await fixture.SeedUserAsync();
|
||||
var playList = await CreatePlayListAsync(owner, PlayListVisibility.Private);
|
||||
|
||||
var response = await fixture.CreateAuthenticatedClient(owner).PutAsJsonAsync(
|
||||
$"/playlists/{playList.Id}", new UpdatePlayListRequest("Renamed", null, null), Ct);
|
||||
var response = await fixture.CreateAuthenticatedClient(owner).PutAsJsonAsync(
|
||||
$"/playlists/{playList.Id}", new UpdatePlayListRequest("Renamed", null, null), Ct);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var body = await response.Content.ReadFromJsonAsync<PlayListApplicationResponse>(JsonOptions, Ct);
|
||||
Assert.Equal("Renamed", body!.Name);
|
||||
}
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var body = await response.Content.ReadFromJsonAsync<PlayListApplicationResponse>(JsonOptions, Ct);
|
||||
Assert.Equal("Renamed", body!.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetPlayListById_NotAGuid_ReturnsNotFound()
|
||||
{
|
||||
var response = await fixture.CreateAnonymousClient().GetAsync("/playlists/not-a-guid", Ct);
|
||||
[Fact]
|
||||
public async Task GetPlayListById_NotAGuid_ReturnsNotFound()
|
||||
{
|
||||
var response = await fixture.CreateAnonymousClient().GetAsync("/playlists/not-a-guid", Ct);
|
||||
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreatePlayList_NameTooLong_ReturnsValidationProblem()
|
||||
{
|
||||
var owner = await fixture.SeedUserAsync();
|
||||
[Fact]
|
||||
public async Task CreatePlayList_NameTooLong_ReturnsValidationProblem()
|
||||
{
|
||||
var owner = await fixture.SeedUserAsync();
|
||||
|
||||
var response = await fixture.CreateAuthenticatedClient(owner).PostAsJsonAsync(
|
||||
"/playlists", new CreatePlayListRequest(new string('a', 51), null, null), Ct);
|
||||
var response = await fixture.CreateAuthenticatedClient(owner).PostAsJsonAsync(
|
||||
"/playlists", new CreatePlayListRequest(new string('a', 51), null, null), Ct);
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
|
||||
private static async Task<IReadOnlyList<Guid>> ReadPlayListIdsAsync(HttpClient client, long userId)
|
||||
{
|
||||
var response = await client.GetAsync($"/playlists/users/{userId}", Ct);
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var page = await response.Content.ReadFromJsonAsync<PaginatedResponse<PlayListApplicationResponse>>(JsonOptions, Ct);
|
||||
return page!.Items.Select(playList => playList.Id).ToList();
|
||||
private static async Task<IReadOnlyList<Guid>> ReadPlayListIdsAsync(HttpClient client, long userId)
|
||||
{
|
||||
var response = await client.GetAsync($"/playlists/users/{userId}", Ct);
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var page = await response.Content.ReadFromJsonAsync<PaginatedResponse<PlayListApplicationResponse>>(JsonOptions, Ct);
|
||||
return page!.Items.Select(playList => playList.Id).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,131 +11,132 @@ using Musify.Domain.Entities;
|
||||
using Musify.Infrastructure.Persistence;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Api.Tests.Endpoints;
|
||||
|
||||
[Collection(ApiCollection.Name)]
|
||||
public sealed class UserEndpointsTests(ApiTestFixture fixture)
|
||||
namespace Musify.Api.Tests.Endpoints
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
[Collection(ApiCollection.Name)]
|
||||
public sealed class UserEndpointsTests(ApiTestFixture fixture)
|
||||
{
|
||||
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
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
Title = trackTitle,
|
||||
NormalizedTitle = trackTitle.ToUpperInvariant(),
|
||||
OwnerUserId = owner.Id,
|
||||
Owner = owner
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
};
|
||||
database.Tracks.Add(track);
|
||||
database.ListeningHistories.Add(new ListeningHistory
|
||||
|
||||
private async Task SeedListenAsync(long userId, string trackTitle, DateTime listenedAt)
|
||||
{
|
||||
UserId = userId,
|
||||
TrackId = track.Id,
|
||||
ListenedAt = listenedAt,
|
||||
IsCounted = true
|
||||
});
|
||||
await database.SaveChangesAsync(TestContext.Current.CancellationToken);
|
||||
}
|
||||
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,
|
||||
IsCounted = true
|
||||
});
|
||||
await database.SaveChangesAsync(TestContext.Current.CancellationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PostUsers_IsNotExposed()
|
||||
{
|
||||
var client = fixture.CreateAnonymousClient();
|
||||
[Fact]
|
||||
public async Task PostUsers_IsNotExposed()
|
||||
{
|
||||
var client = fixture.CreateAnonymousClient();
|
||||
|
||||
var response = await client.PostAsJsonAsync("/users", new { id = 1, name = "Intruder" }, TestContext.Current.CancellationToken);
|
||||
var response = await client.PostAsJsonAsync("/users", new { id = 1, name = "Intruder" }, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.True(response.StatusCode is HttpStatusCode.NotFound or HttpStatusCode.MethodNotAllowed);
|
||||
}
|
||||
Assert.True(response.StatusCode is HttpStatusCode.NotFound or HttpStatusCode.MethodNotAllowed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetUserById_ExistingUser_ReturnsIt()
|
||||
{
|
||||
var client = fixture.CreateAnonymousClient();
|
||||
var userId = Random.Shared.NextInt64(1, long.MaxValue);
|
||||
await fixture.SeedUserAsync(userId, "Findable User");
|
||||
[Fact]
|
||||
public async Task GetUserById_ExistingUser_ReturnsIt()
|
||||
{
|
||||
var client = fixture.CreateAnonymousClient();
|
||||
var userId = Random.Shared.NextInt64(1, long.MaxValue);
|
||||
await fixture.SeedUserAsync(userId, "Findable User");
|
||||
|
||||
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 fixture.SeedUserAsync(userId);
|
||||
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 fixture.SeedUserAsync(userId);
|
||||
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_ReturnsNoContent()
|
||||
{
|
||||
var client = fixture.CreateAnonymousClient();
|
||||
var userId = Random.Shared.NextInt64(1, long.MaxValue);
|
||||
await fixture.SeedUserAsync(userId);
|
||||
[Fact]
|
||||
public async Task GetLastTrackListenedByUserId_NoHistory_ReturnsNoContent()
|
||||
{
|
||||
var client = fixture.CreateAnonymousClient();
|
||||
var userId = Random.Shared.NextInt64(1, long.MaxValue);
|
||||
await fixture.SeedUserAsync(userId);
|
||||
|
||||
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.NoContent, response.StatusCode);
|
||||
}
|
||||
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("pageSize=0")]
|
||||
[InlineData("pageSize=101")]
|
||||
[InlineData("pageNumber=0")]
|
||||
public async Task GetUsers_InvalidPaging_ReturnsBadRequest(string query)
|
||||
{
|
||||
var client = fixture.CreateAnonymousClient();
|
||||
[Theory]
|
||||
[InlineData("pageSize=0")]
|
||||
[InlineData("pageSize=101")]
|
||||
[InlineData("pageNumber=0")]
|
||||
public async Task GetUsers_InvalidPaging_ReturnsBadRequest(string query)
|
||||
{
|
||||
var client = fixture.CreateAnonymousClient();
|
||||
|
||||
var response = await client.GetAsync($"/users?{query}", TestContext.Current.CancellationToken);
|
||||
var response = await client.GetAsync($"/users?{query}", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetListeningStats_AnotherUser_ReturnsForbidden()
|
||||
{
|
||||
var userId = await fixture.SeedUserAsync();
|
||||
var other = await fixture.SeedUserAsync();
|
||||
var client = fixture.CreateAuthenticatedClient(userId);
|
||||
[Fact]
|
||||
public async Task GetListeningStats_AnotherUser_ReturnsForbidden()
|
||||
{
|
||||
var userId = await fixture.SeedUserAsync();
|
||||
var other = await fixture.SeedUserAsync();
|
||||
var client = fixture.CreateAuthenticatedClient(userId);
|
||||
|
||||
var response = await client.GetAsync($"/users/{other}/listening-stats", TestContext.Current.CancellationToken);
|
||||
var response = await client.GetAsync($"/users/{other}/listening-stats", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
|
||||
}
|
||||
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetListeningStats_Anonymous_ReturnsUnauthorized()
|
||||
{
|
||||
var response = await fixture.CreateAnonymousClient().GetAsync("/users/1/listening-stats", TestContext.Current.CancellationToken);
|
||||
[Fact]
|
||||
public async Task GetListeningStats_Anonymous_ReturnsUnauthorized()
|
||||
{
|
||||
var response = await fixture.CreateAnonymousClient().GetAsync("/users/1/listening-stats", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,132 +4,133 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Musify.Api.Extensions;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Api.Tests.Extensions;
|
||||
|
||||
public sealed class ErrorOrHttpExtensionsTests
|
||||
namespace Musify.Api.Tests.Extensions
|
||||
{
|
||||
private static readonly IServiceProvider Services = new ServiceCollection().AddLogging().AddProblemDetails().BuildServiceProvider();
|
||||
|
||||
private static async Task<(int StatusCode, string Body)> ExecuteAsync(IResult result)
|
||||
public sealed class ErrorOrHttpExtensionsTests
|
||||
{
|
||||
var context = new DefaultHttpContext { RequestServices = Services };
|
||||
var bodyStream = new MemoryStream();
|
||||
context.Response.Body = bodyStream;
|
||||
private static readonly IServiceProvider Services = new ServiceCollection().AddLogging().AddProblemDetails().BuildServiceProvider();
|
||||
|
||||
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>
|
||||
private static async Task<(int StatusCode, string Body)> ExecuteAsync(IResult result)
|
||||
{
|
||||
Error.Validation("Name", "Name is required"),
|
||||
Error.Validation("Name", "Name is too long"),
|
||||
Error.Validation("Age", "Age must be positive"),
|
||||
};
|
||||
var context = new DefaultHttpContext { RequestServices = Services };
|
||||
var bodyStream = new MemoryStream();
|
||||
context.Response.Body = bodyStream;
|
||||
|
||||
var (statusCode, body) = await ExecuteAsync(result.ToHttpResult());
|
||||
await result.ExecuteAsync(context);
|
||||
|
||||
Assert.Equal(StatusCodes.Status400BadRequest, statusCode);
|
||||
Assert.Contains("Name is required", body);
|
||||
Assert.Contains("Age must be positive", body);
|
||||
}
|
||||
bodyStream.Position = 0;
|
||||
using var reader = new StreamReader(bodyStream);
|
||||
return (context.Response.StatusCode, await reader.ReadToEndAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ToCreatedResult_Success_ReturnsCreatedAtTheGivenLocation()
|
||||
{
|
||||
ErrorOr<string> result = "the-id";
|
||||
[Fact]
|
||||
public async Task ToHttpResult_Success_ReturnsNoContent()
|
||||
{
|
||||
ErrorOr<Success> result = new Success();
|
||||
|
||||
var context = new DefaultHttpContext { RequestServices = Services, Response = { Body = new MemoryStream() } };
|
||||
await result.ToCreatedResult(value => $"/things/{value}").ExecuteAsync(context);
|
||||
var (statusCode, _) = await ExecuteAsync(result.ToHttpResult());
|
||||
|
||||
Assert.Equal(StatusCodes.Status201Created, context.Response.StatusCode);
|
||||
Assert.Equal("/things/the-id", context.Response.Headers.Location);
|
||||
}
|
||||
Assert.Equal(StatusCodes.Status204NoContent, statusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ToCreatedResult_Error_ReturnsTheMappedErrorStatus()
|
||||
{
|
||||
ErrorOr<string> result = Error.Conflict();
|
||||
[Fact]
|
||||
public async Task ToHttpResult_Value_ReturnsOkWithTheValue()
|
||||
{
|
||||
ErrorOr<string> result = "hello";
|
||||
|
||||
var (statusCode, _) = await ExecuteAsync(result.ToCreatedResult(value => $"/things/{value}"));
|
||||
var (statusCode, body) = await ExecuteAsync(result.ToHttpResult());
|
||||
|
||||
Assert.Equal(StatusCodes.Status409Conflict, statusCode);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,56 +6,57 @@ using Musify.Api.Filters;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Api.Tests.Filters;
|
||||
|
||||
public sealed class ValidationFilterTests
|
||||
namespace Musify.Api.Tests.Filters
|
||||
{
|
||||
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 class ValidationFilterTests
|
||||
{
|
||||
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"));
|
||||
public sealed record Payload(string Name);
|
||||
|
||||
var result = await filter.InvokeAsync(context, _ => ValueTask.FromResult<object?>("next-called"));
|
||||
private static EndpointFilterInvocationContext CreateContext<T>(T argument) =>
|
||||
EndpointFilterInvocationContext.Create(new DefaultHttpContext(), argument);
|
||||
|
||||
Assert.Equal("next-called", result);
|
||||
await validator.Received(1).ValidateAsync(Arg.Is<Payload>(p => p.Name == "ok"), Arg.Any<CancellationToken>());
|
||||
}
|
||||
[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"));
|
||||
|
||||
[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;
|
||||
var result = await filter.InvokeAsync(context, _ => ValueTask.FromResult<object?>("next-called"));
|
||||
|
||||
var result = await filter.InvokeAsync(context, _ => { nextCalled = true; return ValueTask.FromResult<object?>(null); });
|
||||
Assert.Equal("next-called", result);
|
||||
await validator.Received(1).ValidateAsync(Arg.Is<Payload>(p => p.Name == "ok"), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
Assert.False(nextCalled);
|
||||
var problem = Assert.IsType<ProblemHttpResult>(result);
|
||||
Assert.Equal(StatusCodes.Status400BadRequest, problem.StatusCode);
|
||||
}
|
||||
[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;
|
||||
|
||||
[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, _ => { nextCalled = true; return ValueTask.FromResult<object?>(null); });
|
||||
|
||||
var result = await filter.InvokeAsync(context, _ => ValueTask.FromResult<object?>("next-called"));
|
||||
Assert.False(nextCalled);
|
||||
var problem = Assert.IsType<ProblemHttpResult>(result);
|
||||
Assert.Equal(StatusCodes.Status400BadRequest, problem.StatusCode);
|
||||
}
|
||||
|
||||
Assert.Equal("next-called", result);
|
||||
await validator.DidNotReceiveWithAnyArgs().ValidateAsync(Arg.Any<Payload>(), Arg.Any<CancellationToken>());
|
||||
[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>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,108 +12,109 @@ using NSubstitute;
|
||||
using Testcontainers.PostgreSql;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Api.Tests.TestSupport;
|
||||
|
||||
public sealed class ApiTestFixture : WebApplicationFactory<Program>, IAsyncLifetime
|
||||
namespace Musify.Api.Tests.TestSupport
|
||||
{
|
||||
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()
|
||||
public sealed class ApiTestFixture : WebApplicationFactory<Program>, IAsyncLifetime
|
||||
{
|
||||
await postgres.StartAsync();
|
||||
private readonly PostgreSqlContainer postgres = new PostgreSqlBuilder("postgres:17-alpine")
|
||||
.WithDatabase("musify_api_test")
|
||||
.WithUsername("postgres")
|
||||
.WithPassword("postgres")
|
||||
.Build();
|
||||
|
||||
using var scope = Services.CreateScope();
|
||||
var database = scope.ServiceProvider.GetRequiredService<Musify.Infrastructure.Persistence.Database>();
|
||||
await database.Database.MigrateAsync();
|
||||
}
|
||||
public IStorageService StorageService { get; } = Substitute.For<IStorageService>();
|
||||
|
||||
public override async ValueTask DisposeAsync()
|
||||
{
|
||||
await base.DisposeAsync();
|
||||
await postgres.DisposeAsync();
|
||||
}
|
||||
public IEventBus EventBus { get; } = Substitute.For<IEventBus>();
|
||||
|
||||
public HttpClient CreateAnonymousClient() => CreateClient();
|
||||
public IStreamTicketService StreamTicketService { get; } = Substitute.For<IStreamTicketService>();
|
||||
|
||||
public async Task<long> SeedUserAsync(long? userId = null, string? name = null)
|
||||
{
|
||||
var id = userId ?? Random.Shared.NextInt64(1, long.MaxValue);
|
||||
var userName = name ?? $"user-{id}";
|
||||
|
||||
using var scope = Services.CreateScope();
|
||||
var database = scope.ServiceProvider.GetRequiredService<Musify.Infrastructure.Persistence.Database>();
|
||||
database.Users.Add(new Musify.Domain.Entities.User
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
Id = id,
|
||||
Name = userName,
|
||||
NormalizedName = userName.ToUpperInvariant()
|
||||
});
|
||||
await database.SaveChangesAsync();
|
||||
return id;
|
||||
}
|
||||
await postgres.StartAsync();
|
||||
|
||||
public HttpClient CreateAuthenticatedClient(long userId, string userName = "test-user")
|
||||
{
|
||||
var client = CreateClient();
|
||||
client.DefaultRequestHeaders.Add(FakeAuthenticationHandler.UserIdHeader, userId.ToString(CultureInfo.InvariantCulture));
|
||||
client.DefaultRequestHeaders.Add("X-Test-User-Name", userName);
|
||||
return client;
|
||||
}
|
||||
using var scope = Services.CreateScope();
|
||||
var database = scope.ServiceProvider.GetRequiredService<Musify.Infrastructure.Persistence.Database>();
|
||||
await database.Database.MigrateAsync();
|
||||
}
|
||||
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
{
|
||||
builder.ConfigureAppConfiguration((_, configuration) =>
|
||||
public override async ValueTask DisposeAsync()
|
||||
{
|
||||
configuration.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
await base.DisposeAsync();
|
||||
await postgres.DisposeAsync();
|
||||
}
|
||||
|
||||
public HttpClient CreateAnonymousClient() => CreateClient();
|
||||
|
||||
public async Task<long> SeedUserAsync(long? userId = null, string? name = null)
|
||||
{
|
||||
var id = userId ?? Random.Shared.NextInt64(1, long.MaxValue);
|
||||
var userName = name ?? $"user-{id}";
|
||||
|
||||
using var scope = Services.CreateScope();
|
||||
var database = scope.ServiceProvider.GetRequiredService<Musify.Infrastructure.Persistence.Database>();
|
||||
database.Users.Add(new Musify.Domain.Entities.User
|
||||
{
|
||||
["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",
|
||||
Id = id,
|
||||
Name = userName,
|
||||
NormalizedName = userName.ToUpperInvariant()
|
||||
});
|
||||
});
|
||||
await database.SaveChangesAsync();
|
||||
return id;
|
||||
}
|
||||
|
||||
builder.ConfigureTestServices(services =>
|
||||
public HttpClient CreateAuthenticatedClient(long userId, string userName = "test-user")
|
||||
{
|
||||
services.Replace(ServiceDescriptor.Singleton(StorageService));
|
||||
services.Replace(ServiceDescriptor.Scoped(_ => EventBus));
|
||||
services.Replace(ServiceDescriptor.Singleton(StreamTicketService));
|
||||
var client = CreateClient();
|
||||
client.DefaultRequestHeaders.Add(FakeAuthenticationHandler.UserIdHeader, userId.ToString(CultureInfo.InvariantCulture));
|
||||
client.DefaultRequestHeaders.Add("X-Test-User-Name", userName);
|
||||
return client;
|
||||
}
|
||||
|
||||
services
|
||||
.AddAuthentication(FakeAuthenticationHandler.SchemeName)
|
||||
.AddScheme<AuthenticationSchemeOptions, FakeAuthenticationHandler>(FakeAuthenticationHandler.SchemeName, _ => { });
|
||||
});
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
||||
[CollectionDefinition(Name)]
|
||||
public sealed class ApiCollection : ICollectionFixture<ApiTestFixture>
|
||||
{
|
||||
public const string Name = "Api";
|
||||
}
|
||||
|
||||
@@ -4,28 +4,29 @@ using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Musify.Api.Tests.TestSupport;
|
||||
|
||||
public sealed class FakeAuthenticationHandler(
|
||||
IOptionsMonitor<AuthenticationSchemeOptions> options,
|
||||
ILoggerFactory logger,
|
||||
UrlEncoder encoder)
|
||||
: AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder)
|
||||
namespace Musify.Api.Tests.TestSupport
|
||||
{
|
||||
public const string SchemeName = "Test";
|
||||
public const string UserIdHeader = "X-Test-User-Id";
|
||||
|
||||
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
public sealed class FakeAuthenticationHandler(
|
||||
IOptionsMonitor<AuthenticationSchemeOptions> options,
|
||||
ILoggerFactory logger,
|
||||
UrlEncoder encoder)
|
||||
: AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder)
|
||||
{
|
||||
if (!Request.Headers.TryGetValue(UserIdHeader, out var userId) || string.IsNullOrEmpty(userId))
|
||||
return Task.FromResult(AuthenticateResult.NoResult());
|
||||
public const string SchemeName = "Test";
|
||||
public const string UserIdHeader = "X-Test-User-Id";
|
||||
|
||||
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!));
|
||||
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
{
|
||||
if (!Request.Headers.TryGetValue(UserIdHeader, out var userId) || string.IsNullOrEmpty(userId))
|
||||
return Task.FromResult(AuthenticateResult.NoResult());
|
||||
|
||||
var identity = new ClaimsIdentity(claims, SchemeName);
|
||||
var ticket = new AuthenticationTicket(new ClaimsPrincipal(identity), SchemeName);
|
||||
return Task.FromResult(AuthenticateResult.Success(ticket));
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,83 +3,84 @@ using Musify.Api.Validators;
|
||||
using Musify.Api.Validators.Albums;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Api.Tests.Validators;
|
||||
|
||||
public sealed class CreateAlbumRequestValidatorTests
|
||||
namespace Musify.Api.Tests.Validators
|
||||
{
|
||||
private readonly CreateAlbumRequestValidator validator = new();
|
||||
|
||||
[Fact]
|
||||
public void Validate_ValidRequest_Passes()
|
||||
public sealed class CreateAlbumRequestValidatorTests
|
||||
{
|
||||
var result = validator.Validate(new CreateAlbumRequest("My Album", "A short description", 2024, Guid.NewGuid()));
|
||||
private readonly CreateAlbumRequestValidator validator = new();
|
||||
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
[Fact]
|
||||
public void Validate_ValidRequest_Passes()
|
||||
{
|
||||
var result = validator.Validate(new CreateAlbumRequest("My Album", "A short description", 2024, 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_ValidRequestWithoutOptionalFields_Passes()
|
||||
{
|
||||
var result = validator.Validate(new CreateAlbumRequest("My Album", null, null, Guid.NewGuid()));
|
||||
|
||||
[Fact]
|
||||
public void Validate_BlankTitle_Fails()
|
||||
{
|
||||
var result = validator.Validate(new CreateAlbumRequest("", null, null, Guid.NewGuid()));
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Contains(result.Errors, e => e.PropertyName == nameof(CreateAlbumRequest.Title));
|
||||
}
|
||||
[Fact]
|
||||
public void Validate_BlankTitle_Fails()
|
||||
{
|
||||
var result = validator.Validate(new CreateAlbumRequest("", 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.Contains(result.Errors, e => e.PropertyName == nameof(CreateAlbumRequest.Title));
|
||||
}
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
}
|
||||
[Fact]
|
||||
public void Validate_TitleOverMaxLength_Fails()
|
||||
{
|
||||
var result = validator.Validate(new CreateAlbumRequest(new string('a', 201), null, null, Guid.NewGuid()));
|
||||
|
||||
[Fact]
|
||||
public void Validate_ReleaseYearBeforeTheEarliestBoundary_Fails()
|
||||
{
|
||||
var result = validator.Validate(new CreateAlbumRequest("Title", null, Limits.EarliestReleaseYear - 1, Guid.NewGuid()));
|
||||
Assert.False(result.IsValid);
|
||||
}
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
}
|
||||
[Fact]
|
||||
public void Validate_ReleaseYearBeforeTheEarliestBoundary_Fails()
|
||||
{
|
||||
var result = validator.Validate(new CreateAlbumRequest("Title", null, Limits.EarliestReleaseYear - 1, Guid.NewGuid()));
|
||||
|
||||
[Fact]
|
||||
public void Validate_ReleaseYearAtTheEarliestBoundary_Passes()
|
||||
{
|
||||
var result = validator.Validate(new CreateAlbumRequest("Title", null, Limits.EarliestReleaseYear, Guid.NewGuid()));
|
||||
Assert.False(result.IsValid);
|
||||
}
|
||||
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
[Fact]
|
||||
public void Validate_ReleaseYearAtTheEarliestBoundary_Passes()
|
||||
{
|
||||
var result = validator.Validate(new CreateAlbumRequest("Title", null, Limits.EarliestReleaseYear, Guid.NewGuid()));
|
||||
|
||||
[Fact]
|
||||
public void Validate_ReleaseYearAtTheLatestBoundary_Passes()
|
||||
{
|
||||
var result = validator.Validate(new CreateAlbumRequest("Title", null, Limits.LatestReleaseYear, Guid.NewGuid()));
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
[Fact]
|
||||
public void Validate_ReleaseYearAtTheLatestBoundary_Passes()
|
||||
{
|
||||
var result = validator.Validate(new CreateAlbumRequest("Title", null, Limits.LatestReleaseYear, Guid.NewGuid()));
|
||||
|
||||
[Fact]
|
||||
public void Validate_ReleaseYearPastTheLatestBoundary_Fails()
|
||||
{
|
||||
var result = validator.Validate(new CreateAlbumRequest("Title", null, Limits.LatestReleaseYear + 1, Guid.NewGuid()));
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
}
|
||||
[Fact]
|
||||
public void Validate_ReleaseYearPastTheLatestBoundary_Fails()
|
||||
{
|
||||
var result = validator.Validate(new CreateAlbumRequest("Title", null, Limits.LatestReleaseYear + 1, Guid.NewGuid()));
|
||||
|
||||
[Fact]
|
||||
public void Validate_EmptyPictureIntentId_Fails()
|
||||
{
|
||||
var result = validator.Validate(new CreateAlbumRequest("Title", null, null, Guid.Empty));
|
||||
Assert.False(result.IsValid);
|
||||
}
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Contains(result.Errors, e => e.PropertyName == nameof(CreateAlbumRequest.PictureIntentId));
|
||||
[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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,46 +3,47 @@ using Musify.Api.Validators.Tracks;
|
||||
using Musify.Domain.ValueObjects;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Api.Tests.Validators;
|
||||
|
||||
public sealed class TrackRequestValidatorTests
|
||||
namespace Musify.Api.Tests.Validators
|
||||
{
|
||||
[Fact]
|
||||
public void CreateTrack_ValidRequest_Passes()
|
||||
public sealed class TrackRequestValidatorTests
|
||||
{
|
||||
var request = new CreateTrackRequest("Song", Guid.NewGuid(), Guid.NewGuid(), [Genre.Pop, Genre.Rock]);
|
||||
[Fact]
|
||||
public void CreateTrack_ValidRequest_Passes()
|
||||
{
|
||||
var request = new CreateTrackRequest("Song", Guid.NewGuid(), Guid.NewGuid(), [Genre.Pop, Genre.Rock]);
|
||||
|
||||
Assert.True(new CreateTrackRequestValidator().Validate(request).IsValid);
|
||||
}
|
||||
Assert.True(new CreateTrackRequestValidator().Validate(request).IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateTrack_NoTags_Fails()
|
||||
{
|
||||
var request = new CreateTrackRequest("Song", Guid.NewGuid(), Guid.NewGuid(), []);
|
||||
[Fact]
|
||||
public void CreateTrack_NoTags_Fails()
|
||||
{
|
||||
var request = new CreateTrackRequest("Song", Guid.NewGuid(), Guid.NewGuid(), []);
|
||||
|
||||
Assert.False(new CreateTrackRequestValidator().Validate(request).IsValid);
|
||||
}
|
||||
Assert.False(new CreateTrackRequestValidator().Validate(request).IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateTrack_IncompatibleTags_Fails()
|
||||
{
|
||||
var request = new CreateTrackRequest("Song", Guid.NewGuid(), Guid.NewGuid(), [Genre.Classical, Genre.Metal]);
|
||||
[Fact]
|
||||
public void CreateTrack_IncompatibleTags_Fails()
|
||||
{
|
||||
var request = new CreateTrackRequest("Song", Guid.NewGuid(), Guid.NewGuid(), [Genre.Classical, Genre.Metal]);
|
||||
|
||||
Assert.False(new CreateTrackRequestValidator().Validate(request).IsValid);
|
||||
}
|
||||
Assert.False(new CreateTrackRequestValidator().Validate(request).IsValid);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(-1)]
|
||||
[InlineData(double.NaN)]
|
||||
[InlineData(double.PositiveInfinity)]
|
||||
public void RecordListeningProgress_InvalidSeconds_Fails(double seconds)
|
||||
{
|
||||
Assert.False(new RecordListeningProgressRequestValidator().Validate(new RecordListeningProgressRequest(seconds)).IsValid);
|
||||
}
|
||||
[Theory]
|
||||
[InlineData(-1)]
|
||||
[InlineData(double.NaN)]
|
||||
[InlineData(double.PositiveInfinity)]
|
||||
public void RecordListeningProgress_InvalidSeconds_Fails(double seconds)
|
||||
{
|
||||
Assert.False(new RecordListeningProgressRequestValidator().Validate(new RecordListeningProgressRequest(seconds)).IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RecordListeningProgress_ZeroSeconds_Passes()
|
||||
{
|
||||
Assert.True(new RecordListeningProgressRequestValidator().Validate(new RecordListeningProgressRequest(0)).IsValid);
|
||||
[Fact]
|
||||
public void RecordListeningProgress_ZeroSeconds_Passes()
|
||||
{
|
||||
Assert.True(new RecordListeningProgressRequestValidator().Validate(new RecordListeningProgressRequest(0)).IsValid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,45 +5,46 @@ using Musify.Api.Validators.Tracks;
|
||||
using Musify.Application.Configuration;
|
||||
using Xunit;
|
||||
|
||||
namespace Musify.Api.Tests.Validators;
|
||||
|
||||
public sealed class UploadRequestValidatorTests
|
||||
namespace Musify.Api.Tests.Validators
|
||||
{
|
||||
private readonly UploadIntentConfiguration configuration = new() { MaxUploadBytes = 1_000 };
|
||||
|
||||
[Fact]
|
||||
public void PictureUpload_ValidRequest_Passes()
|
||||
public sealed class UploadRequestValidatorTests
|
||||
{
|
||||
var validator = new PictureUploadRequestValidator(configuration);
|
||||
private readonly UploadIntentConfiguration configuration = new() { MaxUploadBytes = 1_000 };
|
||||
|
||||
var result = validator.Validate(new PictureUploadRequest("webp", "image/webp", 500));
|
||||
[Fact]
|
||||
public void PictureUpload_ValidRequest_Passes()
|
||||
{
|
||||
var validator = new PictureUploadRequestValidator(configuration);
|
||||
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
var result = validator.Validate(new PictureUploadRequest("webp", "image/webp", 500));
|
||||
|
||||
[Theory]
|
||||
[InlineData("exe", "image/webp", 500L)]
|
||||
[InlineData("averyveryveryloongextension", "image/webp", 500L)]
|
||||
[InlineData("we bp", "image/webp", 500L)]
|
||||
[InlineData("webp", "text/html", 500L)]
|
||||
[InlineData("webp", "image/webp", 0L)]
|
||||
[InlineData("webp", "image/webp", 1_001L)]
|
||||
public void PictureUpload_InvalidRequest_Fails(string fileType, string contentType, long size)
|
||||
{
|
||||
var validator = new PictureUploadRequestValidator(configuration);
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
|
||||
var result = validator.Validate(new PictureUploadRequest(fileType, contentType, size));
|
||||
[Theory]
|
||||
[InlineData("exe", "image/webp", 500L)]
|
||||
[InlineData("averyveryveryloongextension", "image/webp", 500L)]
|
||||
[InlineData("we bp", "image/webp", 500L)]
|
||||
[InlineData("webp", "text/html", 500L)]
|
||||
[InlineData("webp", "image/webp", 0L)]
|
||||
[InlineData("webp", "image/webp", 1_001L)]
|
||||
public void PictureUpload_InvalidRequest_Fails(string fileType, string contentType, long size)
|
||||
{
|
||||
var validator = new PictureUploadRequestValidator(configuration);
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
}
|
||||
var result = validator.Validate(new PictureUploadRequest(fileType, contentType, size));
|
||||
|
||||
[Fact]
|
||||
public void TrackUpload_AudioTypesAreCheckedAgainstTheAudioAllowList()
|
||||
{
|
||||
var validator = new RequestTrackUploadUrlsRequestValidator(configuration);
|
||||
Assert.False(result.IsValid);
|
||||
}
|
||||
|
||||
Assert.True(validator.Validate(new RequestTrackUploadUrlsRequest("png", "image/png", "mp3", "audio/mpeg", 10, 900)).IsValid);
|
||||
Assert.False(validator.Validate(new RequestTrackUploadUrlsRequest("png", "image/png", "png", "image/png")).IsValid);
|
||||
Assert.False(validator.Validate(new RequestTrackUploadUrlsRequest("mp3", "audio/mpeg", "mp3", "audio/mpeg")).IsValid);
|
||||
[Fact]
|
||||
public void TrackUpload_AudioTypesAreCheckedAgainstTheAudioAllowList()
|
||||
{
|
||||
var validator = new RequestTrackUploadUrlsRequestValidator(configuration);
|
||||
|
||||
Assert.True(validator.Validate(new RequestTrackUploadUrlsRequest("png", "image/png", "mp3", "audio/mpeg", 10, 900)).IsValid);
|
||||
Assert.False(validator.Validate(new RequestTrackUploadUrlsRequest("png", "image/png", "png", "image/png")).IsValid);
|
||||
Assert.False(validator.Validate(new RequestTrackUploadUrlsRequest("mp3", "audio/mpeg", "mp3", "audio/mpeg")).IsValid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user