using System.Text; using DeepDrftAPI.Controllers; using DeepDrftContent.Constants; using DeepDrftContent.FileDatabase.Models; using DeepDrftContent.Processors; using DeepDrftContent.Processors.Opus; using DeepDrftModels.Enums; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using ContentTrackService = DeepDrftContent.TrackContentService; using FileDb = DeepDrftContent.FileDatabase.Services.FileDatabase; namespace DeepDrftTests; /// /// Delivery-layer tests for the Phase 18.3 ?format= stream selector and the Opus seek/setup sidecar /// endpoint on . These exercise the real , the real /// , and the real over temp-directory /// vaults — the same integration posture as . /// /// The SQL-only collaborators (UnifiedTrackService, ITrackService) are passed as null: the /// actions under test (, ) /// only touch the FileDatabase + resolver path, never the SQL services, so standing up a database is not /// required to assert the delivery contract. /// /// The Range→206 contract is asserted at the load-bearing seam: ASP.NET performs the actual byte-slicing /// for any whose .EnableRangeProcessing is true. The lossless /// path proves this via the disk-stream ; the resolved Opus path via the /// in-memory — both must report range processing enabled, the explicit fix /// the 18.2 reviewer flagged for the byte[] path. /// [TestFixture] public class TrackFormatDeliveryTests { private string _testDir = string.Empty; [SetUp] public void SetUp() { _testDir = Path.Combine(Path.GetTempPath(), "TrackFormatDeliveryTests", Guid.NewGuid().ToString()); Directory.CreateDirectory(_testDir); } [TearDown] public void TearDown() { try { Directory.Delete(_testDir, recursive: true); } catch { /* Best-effort cleanup — ignore failures */ } } // --- Format resolution at the endpoint --- [Test] public async Task GetTrack_FormatOpus_WhenOpusArtifactPresent_ServesOpusBytesAndOggContentType() { var (controller, entryKey) = await SeedAsync(withOpus: true, withSidecar: false); var result = await controller.GetTrack(entryKey, format: "opus"); var file = result as FileContentResult; Assert.That(file, Is.Not.Null, "Opus delivery serves an in-memory byte[] (FileContentResult)"); Assert.Multiple(() => { Assert.That(file!.ContentType, Is.EqualTo("audio/ogg"), "Opus bytes must carry the audio/ogg content-type"); Assert.That(file.FileContents, Is.EqualTo(OpusBytes), "The served bytes must be the Opus artifact, not the source"); Assert.That(file.EnableRangeProcessing, Is.True, "Range processing must be enabled on the resolved Opus byte[] path"); }); } // --- The C2 fallback --- [Test] public async Task GetTrack_FormatOpus_WhenNoOpusArtifact_FallsBackToLosslessBytesAndContentType() { // No Opus artifact stored — the resolver degrades to lossless (C2): the listener still gets audio, // never a 404 or silence, and the content-type reports the lossless format actually served. var (controller, entryKey) = await SeedAsync(withOpus: false, withSidecar: false); var result = await controller.GetTrack(entryKey, format: "opus"); var file = result as FileContentResult; Assert.That(file, Is.Not.Null, "The fallback still serves resolved bytes via the byte[] path"); Assert.Multiple(() => { Assert.That(file!.ContentType, Is.EqualTo("audio/wav"), "Fallback content-type must be the lossless source's MIME"); Assert.That(file.FileContents, Is.EqualTo(_sourceWav), "Fallback must serve the lossless source bytes"); Assert.That(file.EnableRangeProcessing, Is.True, "Range processing stays enabled on the fallback path too"); }); } [Test] public async Task GetTrack_NoFormatParam_ServesLosslessDiskStream_ByteIdenticalToPrePhase18() { // The no-format path must be byte-identical to today: the zero-copy disk-stream FileStreamResult, // NOT the resolver's in-memory byte[] path (which would force the whole source into memory). var (controller, entryKey) = await SeedAsync(withOpus: true, withSidecar: false); var result = await controller.GetTrack(entryKey, format: null); var file = result as FileStreamResult; Assert.That(file, Is.Not.Null, "Lossless delivery streams from disk (FileStreamResult), not a byte[]"); Assert.Multiple(() => { Assert.That(file!.ContentType, Is.EqualTo("audio/wav")); Assert.That(file.EnableRangeProcessing, Is.True, "Range→206 must work on the lossless disk-stream path"); }); } [Test] public async Task GetTrack_FormatLossless_TakesTheLosslessDiskStreamPath() { // An explicit format=lossless must behave exactly like no param — the disk-stream path, never Opus. var (controller, entryKey) = await SeedAsync(withOpus: true, withSidecar: false); var result = await controller.GetTrack(entryKey, format: "lossless"); Assert.That(result, Is.InstanceOf(), "format=lossless must take the disk-stream path even when an Opus artifact exists"); } [Test] public async Task GetTrack_FormatOpus_WhenTrackDoesNotExist_Returns404() { var controller = BuildController(await FreshDbAsync()); var result = await controller.GetTrack("no-such-track", format: "opus"); Assert.That(result, Is.InstanceOf(), "When even the lossless source is missing, the Opus request 404s (no audio at all)"); } // --- Sidecar 200 / 404 --- [Test] public async Task GetOpusSeekData_WhenSidecarPresent_Returns200WithRawBytes() { var (controller, entryKey) = await SeedAsync(withOpus: true, withSidecar: true); var result = await controller.GetOpusSeekData(entryKey); var file = result as FileContentResult; Assert.That(file, Is.Not.Null, "A stored sidecar is served as raw bytes"); Assert.Multiple(() => { Assert.That(file!.ContentType, Is.EqualTo("application/octet-stream")); Assert.That(file.FileContents, Is.EqualTo(SidecarBytes), "The served bytes must be the stored sidecar blob"); }); } [Test] public async Task GetOpusSeekData_WhenNoSidecar_Returns404() { var (controller, entryKey) = await SeedAsync(withOpus: true, withSidecar: false); var result = await controller.GetOpusSeekData(entryKey); Assert.That(result, Is.InstanceOf(), "No sidecar → 404, so the client degrades to lossless rather than treating it as an error"); } // --- Fixtures + helpers --- private static readonly byte[] OpusBytes = Encoding.ASCII.GetBytes("OggS-fake-opus-payload-for-delivery-test"); private static readonly byte[] SidecarBytes = Encoding.ASCII.GetBytes("setup-header+seek-index-sidecar-blob"); private byte[] _sourceWav = []; private async Task FreshDbAsync() { var db = await FileDb.FromAsync(_testDir); Assert.That(db, Is.Not.Null); return db!; } // Seeds a track's lossless source in the tracks vault and, optionally, its Opus artifact and sidecar in // the track-opus vault, then returns a controller wired over those real vaults plus the entry key. private async Task<(TrackController Controller, string EntryKey)> SeedAsync(bool withOpus, bool withSidecar) { var db = await FreshDbAsync(); var content = new ContentTrackService(db, new AudioProcessorRouter( new AudioProcessor(), new Mp3AudioProcessor(), new FlacAudioProcessor())); var wavPath = Path.Combine(_testDir, Guid.NewGuid().ToString("N") + ".wav"); _sourceWav = BuildMinimalPcmWav(2.0); await File.WriteAllBytesAsync(wavPath, _sourceWav); var seeded = await content.AddTrackAsync(wavPath, "Track", "Artist"); Assert.That(seeded, Is.Not.Null); var entryKey = seeded!.EntryKey; // GetAudioBinaryAsync re-reads what AddTrackAsync stored, so the bytes we assert the fallback against // are the exact stored source bytes (the processor may normalize the input WAV before storing). var storedSource = await content.GetAudioBinaryAsync(entryKey); Assert.That(storedSource, Is.Not.Null); _sourceWav = storedSource!.Buffer; await db.CreateVaultAsync(VaultConstants.TrackOpus, MediaVaultType.Audio); if (withOpus) { var opus = new AudioBinary(new AudioBinaryParams(OpusBytes, OpusBytes.Length, ".opus", 2.0, 320)); Assert.That( await db.RegisterResourceAsync(VaultConstants.TrackOpus, OpusTranscodeService.OpusAudioKey(entryKey), opus), Is.True); } if (withSidecar) { var sidecar = new MediaBinary(new MediaBinaryParams(SidecarBytes, SidecarBytes.Length, ".opusidx")); Assert.That( await db.RegisterResourceAsync(VaultConstants.TrackOpus, OpusTranscodeService.OpusSidecarKey(entryKey), sidecar), Is.True); } return (BuildController(db, content), entryKey); } private static TrackController BuildController(FileDb db, ContentTrackService? content = null) { content ??= new ContentTrackService(db, new AudioProcessorRouter( new AudioProcessor(), new Mp3AudioProcessor(), new FlacAudioProcessor())); var waveforms = new WaveformProfileService( db, new AudioProcessor(), new RmsLoudnessAlgorithm(), Options.Create(new WaveformProfileOptions()), NullLogger.Instance); var resolver = new TrackFormatResolver(db, content, NullLogger.Instance); // SQL-only collaborators are null: the delivery actions under test never touch them. var controller = new TrackController( content, db, unifiedService: null!, sqlTrackService: null!, waveforms, resolver, stagingDirectory: null!, NullLogger.Instance) { ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() } }; return controller; } // Standard-PCM mono 16-bit 44.1 kHz WAV, full-scale square wave. Same layout as the other suites. private static byte[] BuildMinimalPcmWav(double durationSeconds) { const int sampleRate = 44100; const ushort channels = 1; const ushort bitsPerSample = 16; const ushort blockAlign = channels * (bitsPerSample / 8); const uint byteRate = sampleRate * blockAlign; var frames = (int)(sampleRate * durationSeconds); var data = new byte[frames * blockAlign]; for (var i = 0; i < frames; i++) { var sample = (i % 2 == 0) ? short.MaxValue : short.MinValue; data[i * 2] = (byte)(sample & 0xFF); data[i * 2 + 1] = (byte)((sample >> 8) & 0xFF); } using var ms = new MemoryStream(); using var w = new BinaryWriter(ms, Encoding.ASCII, leaveOpen: true); w.Write(Encoding.ASCII.GetBytes("RIFF")); w.Write((uint)(36 + data.Length)); w.Write(Encoding.ASCII.GetBytes("WAVE")); w.Write(Encoding.ASCII.GetBytes("fmt ")); w.Write(16u); w.Write((ushort)1); // PCM w.Write(channels); w.Write((uint)sampleRate); w.Write(byteRate); w.Write(blockAlign); w.Write(bitsPerSample); w.Write(Encoding.ASCII.GetBytes("data")); w.Write((uint)data.Length); w.Write(data); w.Flush(); return ms.ToArray(); } }