using DeepDrftContent; using DeepDrftContent.Constants; using DeepDrftContent.FileDatabase.Models; using DeepDrftContent.Processors; using DeepDrftContent.Processors.Opus; using DeepDrftModels.Enums; using Microsoft.Extensions.Logging.Abstractions; using FileDb = DeepDrftContent.FileDatabase.Services.FileDatabase; namespace DeepDrftTests; /// /// Integration tests for the Phase 18.2 format resolution + sidecar lookup seam /// () over a real . They exercise the four /// resolution branches the brief specifies — lossless, Opus hit, the C2 Opus→lossless fallback, and /// the unknown-track miss — plus sidecar hit/miss. Artifacts are seeded into the vaults exactly as /// 18.1's stores them (Opus audio under the bare EntryKey, sidecar /// under the -sidecar-suffixed key, the source in the tracks vault), so the test is faithful /// to the real storage convention rather than a stand-in. /// [TestFixture] public class TrackFormatResolverTests { private string _testDir = string.Empty; [SetUp] public void SetUp() { _testDir = Path.Combine(Path.GetTempPath(), "TrackFormatResolverTests", Guid.NewGuid().ToString()); Directory.CreateDirectory(_testDir); } [TearDown] public void TearDown() { try { Directory.Delete(_testDir, recursive: true); } catch { /* Best-effort cleanup — ignore failures */ } } private static TrackFormatResolver CreateResolver(FileDb fileDatabase) { // The resolver only opens vault streams / index checks, which never touch the router — but // TrackContentService requires one, so supply a real router with real processors. var router = new AudioProcessorRouter( new AudioProcessor(), new Mp3AudioProcessor(), new FlacAudioProcessor()); var contentService = new TrackContentService(fileDatabase, router); return new TrackFormatResolver( fileDatabase, contentService, NullLogger.Instance); } // Drains a ResolvedAudio's disk-backed stream to a byte[] and disposes it, so the assertions compare the // bytes actually served (read-path streaming returns an open Stream, not a buffered AudioBinary). private static async Task ReadAllAsync(Stream stream) { await using (stream) { using var ms = new MemoryStream(); await stream.CopyToAsync(ms); return ms.ToArray(); } } // Seeds a source artifact in the tracks vault with the given extension, mirroring how the upload path // stores the original bytes (WAV/MP3/FLAC). Returns the bytes for downstream identity assertions. private static async Task SeedSourceAsync(FileDb db, string entryKey, string extension) { await db.CreateVaultAsync(VaultConstants.Tracks, MediaVaultType.Audio); var bytes = new byte[] { 1, 2, 3, 4, 5 }; var audio = new AudioBinary(new AudioBinaryParams(bytes, bytes.Length, extension, 12.0, 1411)); var ok = await db.RegisterResourceAsync(VaultConstants.Tracks, entryKey, audio); Assert.That(ok, Is.True, "source seed must succeed"); return bytes; } // Seeds the Opus audio + sidecar in the track-opus vault exactly as OpusTranscodeService does: // audio under OpusAudioKey (the bare EntryKey) with the .opus extension, sidecar under OpusSidecarKey. private static async Task<(byte[] opus, byte[] sidecar)> SeedOpusAsync(FileDb db, string entryKey) { var opusBytes = await SeedOpusAudioOnlyAsync(db, entryKey); var sidecarBytes = await SeedOpusSidecarOnlyAsync(db, entryKey); return (opusBytes, sidecarBytes); } // Seeds only the Opus audio half (no sidecar) — a half-derived track, used to prove HasOpusAsync rejects it. private static async Task SeedOpusAudioOnlyAsync(FileDb db, string entryKey) { await db.CreateVaultAsync(VaultConstants.TrackOpus, MediaVaultType.Audio); var opusBytes = new byte[] { 9, 9, 9 }; var opusAudio = new AudioBinary(new AudioBinaryParams( opusBytes, opusBytes.Length, OggOpusConstants.OpusExtension, 12.0, 320)); var audioOk = await db.RegisterResourceAsync( VaultConstants.TrackOpus, OpusTranscodeService.OpusAudioKey(entryKey), opusAudio); Assert.That(audioOk, Is.True, "opus audio seed must succeed"); return opusBytes; } // Seeds only the sidecar half (no Opus audio) — the other half-derived case HasOpusAsync must reject. private static async Task SeedOpusSidecarOnlyAsync(FileDb db, string entryKey) { await db.CreateVaultAsync(VaultConstants.TrackOpus, MediaVaultType.Audio); var sidecarBytes = new byte[] { 7, 7, 7, 7 }; var sidecar = new MediaBinary(new MediaBinaryParams( sidecarBytes, sidecarBytes.Length, OggOpusConstants.SidecarExtension)); var sidecarOk = await db.RegisterResourceAsync( VaultConstants.TrackOpus, OpusTranscodeService.OpusSidecarKey(entryKey), sidecar); Assert.That(sidecarOk, Is.True, "sidecar seed must succeed"); return sidecarBytes; } [Test] public async Task ResolveAsync_Lossless_ReturnsSourceArtifactWithItsRealMime() { var db = (await FileDb.FromAsync(_testDir))!; const string entryKey = "lossless-track"; // A FLAC source — proves the lossless branch does NOT assume WAV: the content-type tracks the // stored extension's MIME, not a hard-coded audio/wav. var bytes = await SeedSourceAsync(db, entryKey, ".flac"); var resolver = CreateResolver(db); var resolved = await resolver.ResolveAsync(entryKey, AudioFormat.Lossless); Assert.That(resolved, Is.Not.Null); Assert.That(resolved!.ResolvedFormat, Is.EqualTo(AudioFormat.Lossless)); Assert.That(resolved.ContentType, Is.EqualTo("audio/flac")); Assert.That(resolved.DidFallBack(AudioFormat.Lossless), Is.False); Assert.That(await ReadAllAsync(resolved.Stream), Is.EqualTo(bytes)); } [Test] public async Task ResolveAsync_OpusWhenArtifactExists_ReturnsOpusWithOggContentType() { var db = (await FileDb.FromAsync(_testDir))!; const string entryKey = "opus-track"; await SeedSourceAsync(db, entryKey, ".wav"); var (opusBytes, _) = await SeedOpusAsync(db, entryKey); var resolver = CreateResolver(db); var resolved = await resolver.ResolveAsync(entryKey, AudioFormat.Opus); Assert.That(resolved, Is.Not.Null); Assert.That(resolved!.ResolvedFormat, Is.EqualTo(AudioFormat.Opus)); Assert.That(resolved.ContentType, Is.EqualTo("audio/ogg")); Assert.That(resolved.DidFallBack(AudioFormat.Opus), Is.False); Assert.That(await ReadAllAsync(resolved.Stream), Is.EqualTo(opusBytes)); } [Test] public async Task ResolveAsync_OpusWhenNoArtifact_FallsBackToLosslessNeverNull() { var db = (await FileDb.FromAsync(_testDir))!; const string entryKey = "no-opus-track"; // Source exists; no Opus artifact has been derived. The C2 rule: degrade to lossless, never 404. var bytes = await SeedSourceAsync(db, entryKey, ".wav"); var resolver = CreateResolver(db); var resolved = await resolver.ResolveAsync(entryKey, AudioFormat.Opus); Assert.That(resolved, Is.Not.Null, "Opus absence must degrade to lossless, not null/404"); Assert.That(resolved!.ResolvedFormat, Is.EqualTo(AudioFormat.Lossless), "the resolved format must reflect what was actually returned"); Assert.That(resolved.ContentType, Is.EqualTo("audio/wav"), "a fallback returns the lossless content-type so the decoder picks the right decoder"); Assert.That(resolved.DidFallBack(AudioFormat.Opus), Is.True); Assert.That(await ReadAllAsync(resolved.Stream), Is.EqualTo(bytes)); } [Test] public async Task ResolveAsync_UnknownTrack_ReturnsNull() { var db = (await FileDb.FromAsync(_testDir))!; var resolver = CreateResolver(db); // No source at all — the one case the caller maps to 404. Holds for both requested formats: // Opus falls back to lossless, finds nothing, and returns null too. Assert.That(await resolver.ResolveAsync("ghost", AudioFormat.Lossless), Is.Null); Assert.That(await resolver.ResolveAsync("ghost", AudioFormat.Opus), Is.Null); } [Test] public async Task GetOpusSidecarAsync_WhenPresent_ReturnsBytes() { var db = (await FileDb.FromAsync(_testDir))!; const string entryKey = "sidecar-track"; var (_, sidecarBytes) = await SeedOpusAsync(db, entryKey); var resolver = CreateResolver(db); var bytes = await resolver.GetOpusSidecarAsync(entryKey); Assert.That(bytes, Is.Not.Null); Assert.That(bytes, Is.EqualTo(sidecarBytes)); } [Test] public async Task GetOpusSidecarAsync_WhenAbsent_ReturnsNull() { var db = (await FileDb.FromAsync(_testDir))!; var resolver = CreateResolver(db); // No Opus artifacts derived for this track — the sidecar lookup misses to null, not an exception. var bytes = await resolver.GetOpusSidecarAsync("no-sidecar-track"); Assert.That(bytes, Is.Null); } // --- HasOpusAsync completeness matrix (both halves required) --- [Test] public async Task HasOpusAsync_WhenAudioAndSidecarPresent_ReturnsTrue() { var db = (await FileDb.FromAsync(_testDir))!; const string entryKey = "complete-opus"; await SeedOpusAsync(db, entryKey); var resolver = CreateResolver(db); Assert.That(await resolver.HasOpusAsync(entryKey), Is.True); } [Test] public async Task HasOpusAsync_WhenSidecarMissing_ReturnsFalse() { var db = (await FileDb.FromAsync(_testDir))!; const string entryKey = "audio-only-opus"; await SeedOpusAudioOnlyAsync(db, entryKey); var resolver = CreateResolver(db); Assert.That(await resolver.HasOpusAsync(entryKey), Is.False, "audio without the seek/setup sidecar is unseekable — counts as not-having-Opus"); } [Test] public async Task HasOpusAsync_WhenAudioMissing_ReturnsFalse() { var db = (await FileDb.FromAsync(_testDir))!; const string entryKey = "sidecar-only-opus"; await SeedOpusSidecarOnlyAsync(db, entryKey); var resolver = CreateResolver(db); Assert.That(await resolver.HasOpusAsync(entryKey), Is.False); } [Test] public async Task HasOpusAsync_WhenNeitherPresent_ReturnsFalse() { var db = (await FileDb.FromAsync(_testDir))!; var resolver = CreateResolver(db); // No track-opus vault at all (and no entries) — must miss to false, not throw. Assert.That(await resolver.HasOpusAsync("ghost"), Is.False); } [Test] public async Task HasOpusAsync_IsIndexOnly_DoesNotReadFileBodies() { var db = (await FileDb.FromAsync(_testDir))!; const string entryKey = "indexed-but-bodiless"; await SeedOpusAsync(db, entryKey); var resolver = CreateResolver(db); // Delete the backing files but leave the index entries intact. An index-only existence check still // reports true; a body-loading implementation would now miss. This is the load-bearing assertion // that HasOpusAsync never streams a file body (the opus-status loop runs it over the whole catalogue). var vaultDir = Path.Combine(_testDir, VaultConstants.TrackOpus); foreach (var file in Directory.EnumerateFiles(vaultDir)) { if (Path.GetFileName(file) != "index") File.Delete(file); } Assert.That(await resolver.HasOpusAsync(entryKey), Is.True, "HasOpusAsync must be index-only: true even when no file bodies are present"); } [Test] public async Task ResolveAsync_DisposingResult_DisposesUnderlyingStream() { var db = (await FileDb.FromAsync(_testDir))!; const string entryKey = "dispose-track"; await SeedSourceAsync(db, entryKey, ".wav"); var resolver = CreateResolver(db); var resolved = await resolver.ResolveAsync(entryKey, AudioFormat.Lossless); Assert.That(resolved, Is.Not.Null); var stream = resolved!.Stream; Assert.That(stream.CanRead, Is.True, "a freshly resolved handle holds an open stream"); await resolved.DisposeAsync(); Assert.That(stream.CanRead, Is.False, "disposing ResolvedAudio must dispose the underlying FileStream so a handle never leaks on the throw path"); } }