Tracks
- Track.IsPublished (migration marks existing tracks as published). New
tracks are created unpublished.
- PUT /tracks/{id}/published and PUT /tracks/published change the flag;
publishing requires completed audio processing (the batch endpoint
reports skipped tracks).
- An unpublished track is only visible to its owner. A WherePublished /
VisibleTo filter is applied to search, genres, listening history, last
listened, likes, mixes, album and playlist contents and counts, recently
listened albums, public user tracks, track details and stream tickets.
- isPublished is part of the track responses, GET /tracks/users/{userId}
accepts a published filter and status-counts reports published and
unpublished totals.
- CreateTrackRequest.pictureIntentId is optional: without it the track
uses the album cover or the preset picture and no picture processing
runs. CreateTrackResourcesEvent carries nullable picture fields, so
queues must be drained before deploying.
- POST /tracks/upload-urls accepts an audios list and one optional picture
shared by all of them, and returns an intent per file. The upload intent
limits are raised so a batch fits.
Albums
- AlbumType (Album, EP, Single) on create, update and responses.
- CreateAlbumRequest.trackIds creates the album and links the tracks in a
single save.
120 lines
5.2 KiB
C#
120 lines
5.2 KiB
C#
using ErrorOr;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Musify.Application.Contracts;
|
|
using Musify.Application.Services;
|
|
using Musify.Application.Tests.TestSupport;
|
|
using Musify.Application.Tracks;
|
|
using Musify.Domain.ValueObjects;
|
|
using NSubstitute;
|
|
using Xunit;
|
|
|
|
namespace Musify.Application.Tests.Tracks
|
|
{
|
|
public sealed class RequestTrackUploadUrlsBatchTests : HandlerTestBase
|
|
{
|
|
private static CancellationToken Ct => TestContext.Current.CancellationToken;
|
|
|
|
private readonly IStorageService _storageService = Substitute.For<IStorageService>();
|
|
|
|
private RequestTrackUploadUrlsCommandHandler CreateHandler()
|
|
{
|
|
_storageService
|
|
.GetUploadUrlAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<TimeSpan>(), Arg.Any<bool>(), Arg.Any<CancellationToken>())
|
|
.Returns(call => $"https://storage.musify.test/{call.ArgAt<string>(1)}");
|
|
|
|
var config = TestConfigurations.UploadIntent();
|
|
return new RequestTrackUploadUrlsCommandHandler(
|
|
new UploadIntentIssuer(
|
|
Database,
|
|
_storageService,
|
|
new UploadIntentValidator(Database, _storageService, config),
|
|
TestConfigurations.Storage(),
|
|
config,
|
|
NoOpLogger<UploadIntentIssuer>()),
|
|
TestConfigurations.Storage(),
|
|
TestConfigurations.Track(),
|
|
config);
|
|
}
|
|
|
|
private static readonly AudioUploadSpec[] _threeAudios =
|
|
[
|
|
new("mp3", "audio/mpeg", 3_000_000),
|
|
new("flac", "audio/flac", 30_000_000),
|
|
new("wav", "audio/wav", 40_000_000)
|
|
];
|
|
|
|
[Fact]
|
|
public async Task Handle_ManyAudiosAndSharedPicture_IssuesOneAudioIntentPerFileAndOnePictureIntentPerTrackOnASingleObject()
|
|
{
|
|
var user = TestEntities.User();
|
|
await SeedAsync(user);
|
|
|
|
var result = await CreateHandler().Handle(
|
|
new RequestTrackUploadUrlsCommand(user.Id, "webp", "image/webp", Audios: _threeAudios), Ct);
|
|
|
|
Assert.False(result.IsError);
|
|
Assert.Equal(3, result.Value.Audios.Count);
|
|
Assert.All(result.Value.Audios, audio => Assert.NotNull(audio.PictureIntentId));
|
|
|
|
var intents = await Database.UploadIntents.AsNoTracking().ToListAsync(Ct);
|
|
var pictures = intents.Where(intent => intent.Purpose == UploadIntentPurpose.TrackPicture).ToList();
|
|
var audios = intents.Where(intent => intent.Purpose == UploadIntentPurpose.TrackAudio).ToList();
|
|
Assert.Equal(3, pictures.Count);
|
|
Assert.Equal(3, audios.Count);
|
|
|
|
Assert.Single(pictures.Select(intent => intent.Key).Distinct());
|
|
Assert.Equal(3, pictures.Select(intent => intent.ObjectName).Distinct().Count());
|
|
Assert.Equal(3, audios.Select(intent => intent.Key).Distinct().Count());
|
|
Assert.Equal(
|
|
result.Value.Audios.Select(audio => audio.PictureIntentId!.Value).Order(),
|
|
pictures.Select(intent => intent.Id).Order());
|
|
Assert.Equal(result.Value.Audios.Select(audio => audio.IntentId).Order(), audios.Select(intent => intent.Id).Order());
|
|
Assert.Equal(result.Value.Audios[0].IntentId, result.Value.AudioIntentId);
|
|
Assert.Equal(result.Value.Audios[0].PictureIntentId, result.Value.PictureIntentId);
|
|
Assert.Equal(pictures[0].Key, result.Value.PictureKey);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Handle_ManyAudiosWithoutPicture_IssuesNoPictureIntents()
|
|
{
|
|
var user = TestEntities.User();
|
|
await SeedAsync(user);
|
|
|
|
var result = await CreateHandler().Handle(
|
|
new RequestTrackUploadUrlsCommand(user.Id, null, null, Audios: _threeAudios), Ct);
|
|
|
|
Assert.False(result.IsError);
|
|
Assert.Null(result.Value.PictureIntentId);
|
|
Assert.Null(result.Value.PictureUploadUrl);
|
|
Assert.All(result.Value.Audios, audio => Assert.Null(audio.PictureIntentId));
|
|
Assert.All(await Database.UploadIntents.ToListAsync(Ct), intent => Assert.Equal(UploadIntentPurpose.TrackAudio, intent.Purpose));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Handle_LegacyAudioFields_StillReturnASingleItem()
|
|
{
|
|
var user = TestEntities.User();
|
|
await SeedAsync(user);
|
|
|
|
var result = await CreateHandler().Handle(new RequestTrackUploadUrlsCommand(user.Id, "webp", "image/webp", "mp3", "audio/mpeg"), Ct);
|
|
|
|
Assert.False(result.IsError);
|
|
var item = Assert.Single(result.Value.Audios);
|
|
Assert.Equal(result.Value.AudioIntentId, item.IntentId);
|
|
Assert.Equal(result.Value.PictureIntentId, item.PictureIntentId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Handle_NoAudio_ReturnsValidationError()
|
|
{
|
|
var user = TestEntities.User();
|
|
await SeedAsync(user);
|
|
|
|
var result = await CreateHandler().Handle(new RequestTrackUploadUrlsCommand(user.Id, "webp", "image/webp"), Ct);
|
|
|
|
Assert.Equal(ErrorType.Validation, result.FirstError.Type);
|
|
Assert.Empty(await Database.UploadIntents.ToListAsync(Ct));
|
|
}
|
|
}
|
|
}
|