using System.IO; using System.IO.Compression; using Xunit; using ytLive.Services.Encoder; namespace ytLive.Tests; /// /// The FFmpeg locator (TASK 4 ship step 2): resolves ffmpeg.exe by probing PATH /// first, then the cache in the tools directory, then pulling the pinned BtbN /// LGPL-shared zip. The integration test drives the full ladder against a temp /// tools dir and a fake downloader that returns a real in-memory zip; the units /// pin down the failure and edge cases. No network, no real binary. /// public class FfmpegLocatorTests { private static byte[] MakeZip( string exePath = "ffmpeg-master-latest-win64-lgpl-shared/bin/ffmpeg.exe", string[]? dllPaths = null) { using var ms = new MemoryStream(); using (var archive = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true)) { var entry = archive.CreateEntry(exePath); using (var writer = new StreamWriter(entry.Open())) writer.Write("dummy ffmpeg binary"); foreach (var dll in dllPaths ?? Array.Empty()) { var dllEntry = archive.CreateEntry(dll); using var dllWriter = new StreamWriter(dllEntry.Open()); dllWriter.Write("dummy dll"); } } return ms.ToArray(); } private static string TempDir() { var dir = Path.Combine(Path.GetTempPath(), "ytllive-tests-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(dir); return dir; } private sealed class RecordingDownloader { public int Calls { get; private set; } public byte[] Payload { get; set; } = MakeZip(); public Exception? Error { get; set; } public Task DownloadAsync(string url, CancellationToken ct) { Calls++; if (Error != null) throw Error; return Task.FromResult(Payload); } } [Fact] public async Task Locate_Integration_FullDecisionLadder() { var toolsDir = TempDir(); try { // 1. PATH hit wins, downloader never invoked. var pathDir = TempDir(); var pathExe = Path.Combine(pathDir, FfmpegLocator.FileName); File.WriteAllText(pathExe, "user's ffmpeg"); var downloader = new RecordingDownloader(); var fromPath = new FfmpegLocator([pathDir], toolsDir, downloader.DownloadAsync); Assert.Equal(pathExe, await fromPath.LocateAsync()); Assert.Equal(0, downloader.Calls); // 2. Cache hit skips the network. var cached = Path.Combine(toolsDir, FfmpegLocator.FileName); Directory.CreateDirectory(toolsDir); File.WriteAllText(cached, "cached ffmpeg"); var fromCache = new FfmpegLocator([], toolsDir, downloader.DownloadAsync); Assert.Equal(cached, await fromCache.LocateAsync()); Assert.Equal(0, downloader.Calls); // 3. Cold cache downloads exactly once, extracts ffmpeg.exe, and the // second call serves the cache without re-downloading. File.Delete(cached); var coldTools = TempDir(); var cold = new FfmpegLocator([], coldTools, downloader.DownloadAsync); var resolved = await cold.LocateAsync(); Assert.Equal(Path.Combine(coldTools, FfmpegLocator.FileName), resolved); Assert.Equal(1, downloader.Calls); Assert.True(new FileInfo(resolved).Length > 0); Assert.Equal(resolved, await cold.LocateAsync()); Assert.Equal(1, downloader.Calls); } finally { Directory.Delete(toolsDir, recursive: true); } } [Fact] public async Task Locate_ZeroByteCache_IsRefreshed() { var toolsDir = TempDir(); try { File.WriteAllText(Path.Combine(toolsDir, FfmpegLocator.FileName), ""); var downloader = new RecordingDownloader(); var locator = new FfmpegLocator([], toolsDir, downloader.DownloadAsync); var resolved = await locator.LocateAsync(); Assert.Equal(1, downloader.Calls); Assert.True(new FileInfo(resolved).Length > 0); } finally { Directory.Delete(toolsDir, recursive: true); } } [Fact] public async Task Locate_SharedBuild_ExtractsDllsAlongsideExe() { var toolsDir = TempDir(); try { var downloader = new RecordingDownloader { Payload = MakeZip(dllPaths: [ "ffmpeg-master-latest-win64-lgpl-shared/bin/avcodec-61.dll", "ffmpeg-master-latest-win64-lgpl-shared/bin/avformat-61.dll", ]) }; var locator = new FfmpegLocator([], toolsDir, downloader.DownloadAsync); var resolved = await locator.LocateAsync(); Assert.True(File.Exists(Path.Combine(toolsDir, "avcodec-61.dll"))); Assert.True(File.Exists(Path.Combine(toolsDir, "avformat-61.dll"))); Assert.Equal(1, downloader.Calls); } finally { Directory.Delete(toolsDir, recursive: true); } } [Fact] public async Task Locate_EmptyPayload_Throws() { var downloader = new RecordingDownloader { Payload = Array.Empty() }; var locator = new FfmpegLocator([], TempDir(), downloader.DownloadAsync); await Assert.ThrowsAsync(() => locator.LocateAsync()); Assert.Equal(1, downloader.Calls); } [Fact] public async Task Locate_ZipWithoutFfmpegEntry_Throws() { var downloader = new RecordingDownloader { Payload = MakeZip(exePath: "readme.txt") }; var locator = new FfmpegLocator([], TempDir(), downloader.DownloadAsync); await Assert.ThrowsAsync(() => locator.LocateAsync()); } [Fact] public async Task Locate_DownloaderFailure_Propagates() { var downloader = new RecordingDownloader { Error = new HttpRequestException("offline") }; var locator = new FfmpegLocator([], TempDir(), downloader.DownloadAsync); await Assert.ThrowsAsync(() => locator.LocateAsync()); } }