Files
deepdrft/DeepDrftTests/MediumWritePathTests.cs
T

217 lines
9.8 KiB
C#

using Data.Data.Repositories;
using Data.Managers;
using DeepDrftData;
using DeepDrftData.Data;
using DeepDrftData.Repositories;
using DeepDrftModels.DTOs;
using DeepDrftModels.Entities;
using DeepDrftModels.Enums;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.Abstractions;
using Models.Common;
namespace DeepDrftTests;
/// <summary>
/// Phase 9.5 medium write-path coverage. Exercises the SQL layer that carries the medium through the
/// upload and edit flows (TrackManager + TrackRepository), plus the releaseId track-resolution filter
/// (9.5.C). Runs on the EF in-memory provider, which executes every predicate here in process —
/// release creation, the no-mutation-on-find rule, the medium update + ReleaseType reset, and exact
/// releaseId equality.
///
/// The controller-level form/JSON parse and the ReleaseType-reset conditional (9.5.B) live in
/// TrackController; this fixture asserts the persisted outcome of that logic by driving the same
/// service surface the controller calls (FindOrCreateRelease for upload, ITrackService.Update for
/// the meta edit), so a regression in the data layer that backs the medium write path is caught.
/// </summary>
[TestFixture]
public class MediumWritePathTests
{
private DeepDrftContext _context = null!;
[SetUp]
public void SetUp()
{
var options = new DbContextOptionsBuilder<DeepDrftContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
_context = new DeepDrftContext(options);
}
[TearDown]
public void TearDown() => _context.Dispose();
private TrackRepository CreateRepository()
=> new(_context, NullLogger<Repository<DeepDrftContext, TrackEntity>>.Instance);
private TrackManager CreateManager(TrackRepository repository)
=> new(repository, NullLogger<Manager<TrackEntity, TrackDto, TrackRepository, TrackConverter>>.Instance);
private static ReleaseDto ReleaseData(string title, string artist, ReleaseMedium medium)
=> new() { Title = title, Artist = artist, Medium = medium };
// 9.5.A — a Session upload creates a release carrying Medium == Session.
[Test]
public async Task FindOrCreateRelease_NewSessionRelease_PersistsMediumSession()
{
var manager = CreateManager(CreateRepository());
var result = await manager.FindOrCreateRelease(
"Live at the Vault", "Artist A", ReleaseData("Live at the Vault", "Artist A", ReleaseMedium.Session));
Assert.That(result.Success, Is.True);
Assert.That(result.Value!.Medium, Is.EqualTo(ReleaseMedium.Session));
var stored = await CreateRepository().GetReleaseByIdAsync(result.Value.Id);
Assert.That(stored!.Medium, Is.EqualTo(ReleaseMedium.Session));
}
// 9.5.A — a Mix upload creates a release carrying Medium == Mix.
[Test]
public async Task FindOrCreateRelease_NewMixRelease_PersistsMediumMix()
{
var manager = CreateManager(CreateRepository());
var result = await manager.FindOrCreateRelease(
"Sunset Set", "DJ B", ReleaseData("Sunset Set", "DJ B", ReleaseMedium.Mix));
Assert.That(result.Value!.Medium, Is.EqualTo(ReleaseMedium.Mix));
}
// 9.5.A — a Cut upload (the default) creates a release carrying Medium == Cut.
[Test]
public async Task FindOrCreateRelease_NewCutRelease_PersistsMediumCut()
{
var manager = CreateManager(CreateRepository());
var result = await manager.FindOrCreateRelease(
"Studio Album", "Artist C", ReleaseData("Studio Album", "Artist C", ReleaseMedium.Cut));
Assert.That(result.Value!.Medium, Is.EqualTo(ReleaseMedium.Cut));
}
// 9.5.A — a second upload to an existing release does NOT mutate the stored medium. The first
// upload's medium is authoritative; a Cut-typed follow-up upload must not flip a Session release.
[Test]
public async Task FindOrCreateRelease_ExistingRelease_DoesNotMutateMedium()
{
var repo = CreateRepository();
var manager = CreateManager(repo);
var created = await manager.FindOrCreateRelease(
"Live at the Vault", "Artist A", ReleaseData("Live at the Vault", "Artist A", ReleaseMedium.Session));
// Second add to the same (title, artist) arrives carrying Cut — the find path must ignore it.
var found = await manager.FindOrCreateRelease(
"Live at the Vault", "Artist A", ReleaseData("Live at the Vault", "Artist A", ReleaseMedium.Cut));
Assert.That(found.Value!.Id, Is.EqualTo(created.Value!.Id), "same release row is returned");
Assert.That(found.Value.Medium, Is.EqualTo(ReleaseMedium.Session), "medium stays as first set");
var stored = await CreateRepository().GetReleaseByIdAsync(created.Value.Id);
Assert.That(stored!.Medium, Is.EqualTo(ReleaseMedium.Session), "DB row unchanged");
}
// 9.5.B — updating a track's release to a non-Cut medium persists the new medium. Mirrors the
// PUT api/track/meta apply: the controller sets release.Medium, the manager saves the linked release.
[Test]
public async Task Update_FlipsCutReleaseToSession_PersistsMedium()
{
var repo = CreateRepository();
ITrackService manager = CreateManager(repo);
var release = new ReleaseEntity
{
Title = "Originally a Cut", Artist = "Artist A",
Medium = ReleaseMedium.Cut, ReleaseType = ReleaseType.EP,
};
var track = new TrackEntity { EntryKey = "ek-1", TrackName = "Track", Release = release };
_context.Tracks.Add(track);
await _context.SaveChangesAsync();
var loaded = (await manager.GetById(track.Id)).Value!;
loaded.Release!.Medium = ReleaseMedium.Session;
// The controller resets ReleaseType to the default when medium goes non-Cut; replicate so the
// edited DTO matches what the controller would persist.
loaded.Release.ReleaseType = ReleaseType.Single;
var result = await manager.Update(loaded);
Assert.That(result.Success, Is.True);
var stored = await CreateRepository().GetReleaseByIdAsync(release.Id);
Assert.That(stored!.Medium, Is.EqualTo(ReleaseMedium.Session));
Assert.That(stored.ReleaseType, Is.EqualTo(ReleaseType.Single), "ReleaseType reset to default for a non-Cut medium");
}
// 9.5.B — the read-path converter already enforces the ReleaseType-only-for-Cut invariant: a
// non-Cut release surfaces a null ReleaseType regardless of the stale column value. This is the
// invariant the write-path reset mirrors, asserted at the single mapping point.
[Test]
public void Convert_NonCutRelease_NullsReleaseTypeOnRead()
{
var sessionWithStaleType = new ReleaseEntity
{
Title = "Session", Artist = "A",
Medium = ReleaseMedium.Session, ReleaseType = ReleaseType.Album,
};
var dto = TrackConverter.Convert(sessionWithStaleType);
Assert.That(dto.Medium, Is.EqualTo(ReleaseMedium.Session));
Assert.That(dto.ReleaseType, Is.Null);
}
// 9.5.C — releaseId filter returns only the tracks of the given release. Built on the repository
// directly to assert the WHERE release_id predicate in isolation.
[Test]
public async Task GetPagedFilteredAsync_WithReleaseId_ReturnsOnlyThatReleasesTracks()
{
var first = new ReleaseEntity { Title = "Untitled", Artist = "Artist A" };
var second = new ReleaseEntity { Title = "Untitled", Artist = "Artist B" };
_context.Tracks.AddRange(
new TrackEntity { EntryKey = "a1", TrackName = "A-One", Release = first },
new TrackEntity { EntryKey = "a2", TrackName = "A-Two", Release = first },
new TrackEntity { EntryKey = "b1", TrackName = "B-One", Release = second });
await _context.SaveChangesAsync();
var repo = CreateRepository();
var paging = new PagingParameters<TrackEntity> { Page = 1, PageSize = 20, OrderBy = t => t.Id };
var result = await repo.GetPagedFilteredAsync(paging, new TrackFilter { ReleaseId = first.Id });
Assert.That(result.TotalCount, Is.EqualTo(2));
Assert.That(result.Items.Select(t => t.TrackName), Is.EquivalentTo(new[] { "A-One", "A-Two" }));
}
// 9.5.C — two same-titled releases resolve distinctly by id, the exact failure album-title join
// could not survive. Each releaseId returns only its own track.
[Test]
public async Task GetPagedFilteredAsync_SameTitledReleases_ResolveDistinctlyById()
{
var first = new ReleaseEntity { Title = "Untitled", Artist = "Artist A" };
var second = new ReleaseEntity { Title = "Untitled", Artist = "Artist B" };
_context.Tracks.AddRange(
new TrackEntity { EntryKey = "a1", TrackName = "A-One", Release = first },
new TrackEntity { EntryKey = "b1", TrackName = "B-One", Release = second });
await _context.SaveChangesAsync();
var repo = CreateRepository();
var paging = new PagingParameters<TrackEntity> { Page = 1, PageSize = 20, OrderBy = t => t.Id };
var firstResult = await repo.GetPagedFilteredAsync(paging, new TrackFilter { ReleaseId = first.Id });
var secondResult = await repo.GetPagedFilteredAsync(paging, new TrackFilter { ReleaseId = second.Id });
Assert.That(firstResult.Items.Single().TrackName, Is.EqualTo("A-One"));
Assert.That(secondResult.Items.Single().TrackName, Is.EqualTo("B-One"));
}
// 9.5.C — TrackFilter.IsEmpty accounts for ReleaseId, so a releaseId-only filter is not collapsed
// to a null passthrough by the manager's effectiveFilter guard.
[Test]
public void TrackFilter_WithOnlyReleaseId_IsNotEmpty()
{
Assert.That(new TrackFilter { ReleaseId = 5 }.IsEmpty, Is.False);
Assert.That(new TrackFilter().IsEmpty, Is.True);
}
}