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 calls GetAudioBinaryAsync (a vault read), which never touches 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); } // 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) { 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"); 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 (opusBytes, 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.Audio.Buffer, Is.EqualTo(bytes)); Assert.That(resolved.DidFallBack(AudioFormat.Lossless), Is.False); } [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.Audio.Buffer, Is.EqualTo(opusBytes)); Assert.That(resolved.DidFallBack(AudioFormat.Opus), Is.False); } [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.Audio.Buffer, Is.EqualTo(bytes)); Assert.That(resolved.DidFallBack(AudioFormat.Opus), Is.True); } [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); } }