using System; using System.IO; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; using Paca.Core.Config; using Paca.Core.Hls; using Xunit; namespace Paca.Tests.Core; public class DirectDownloaderIntegrityGateTests : IDisposable { private readonly string _testDir; public DirectDownloaderIntegrityGateTests() { _testDir = Path.Combine(Path.GetTempPath(), "paca_direct_test_" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(_testDir); } public void Dispose() { try { if (Directory.Exists(_testDir)) Directory.Delete(_testDir, recursive: true); } catch { } } [Fact] public void Gate471_DirectDownloader_GuessExt_DetectsExtensions_FromUrlAndMediaType() { // Direct extension in URL Assert.Equal(".mp4", DirectDownloader.GuessExt("", new Uri("https://paca.stream/video.mp4"))); Assert.Equal(".webm", DirectDownloader.GuessExt("", new Uri("https://paca.stream/clip.webm"))); Assert.Equal(".mkv", DirectDownloader.GuessExt("", new Uri("https://paca.stream/movie.mkv"))); Assert.Equal(".mp3", DirectDownloader.GuessExt("", new Uri("https://paca.stream/audio.mp3"))); Assert.Equal(".flac", DirectDownloader.GuessExt("", new Uri("https://paca.stream/track.flac"))); // Fallback from MIME ContentType Assert.Equal(".mp4", DirectDownloader.GuessExt("video/mp4", new Uri("https://paca.stream/stream?id=123"))); Assert.Equal(".webm", DirectDownloader.GuessExt("video/webm", new Uri("https://paca.stream/stream?id=123"))); Assert.Equal(".m4a", DirectDownloader.GuessExt("audio/mp4", new Uri("https://paca.stream/stream?id=123"))); } [Fact] public async Task Gate472_DirectDownloader_VerifiesChecksum_WhenProvided_AndMatches() { var content = "PACA-DIRECT-DOWNLOAD-VERIFIED-DATA"; var bytes = Encoding.UTF8.GetBytes(content); var sha256 = Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant(); var handler = new SimpleMockHttp(bytes); using var client = new HttpClient(handler); var config = new AppConfig(); var downloader = new DirectDownloader(client, config); var outputBase = Path.Combine(_testDir, "test_success"); var resultPath = await downloader.DownloadAsync( new Uri("https://paca.test/file.mp4"), outputBase, progress: null, ct: CancellationToken.None, expectedSha256: sha256 ); Assert.True(File.Exists(resultPath)); Assert.Equal(bytes.Length, new FileInfo(resultPath).Length); Assert.False(File.Exists(outputBase + ".part")); } [Fact] public async Task Gate473_DirectDownloader_ThrowsInvalidDataException_WhenChecksumMismatches() { var content = "TAMPERED-DATA-PAYLOAD"; var bytes = Encoding.UTF8.GetBytes(content); var handler = new SimpleMockHttp(bytes); using var client = new HttpClient(handler); var config = new AppConfig(); var downloader = new DirectDownloader(client, config); var outputBase = Path.Combine(_testDir, "test_corrupt"); await Assert.ThrowsAsync(async () => { await downloader.DownloadAsync( new Uri("https://paca.test/file.mp4"), outputBase, progress: null, ct: CancellationToken.None, expectedSha256: "0000000000000000000000000000000000000000000000000000000000000000" ); }); } [Fact] public async Task Gate474_DirectDownloader_AtomicPartCommit_CleansPartFile_UponCompletion() { var content = "PACA-ATOMIC-COMMIT-STREAM"; var bytes = Encoding.UTF8.GetBytes(content); var handler = new SimpleMockHttp(bytes); using var client = new HttpClient(handler); var config = new AppConfig(); var downloader = new DirectDownloader(client, config); var outputBase = Path.Combine(_testDir, "atomic_output"); var final = await downloader.DownloadAsync( new Uri("https://paca.test/video.mp4"), outputBase ); Assert.True(File.Exists(final)); Assert.False(File.Exists(outputBase + ".part")); } private sealed class SimpleMockHttp : HttpMessageHandler { private readonly byte[] _data; public SimpleMockHttp(byte[] data) { _data = data; } protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var response = new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(_data) }; response.Content.Headers.ContentType = new MediaTypeHeaderValue("video/mp4"); response.Content.Headers.ContentLength = _data.Length; return Task.FromResult(response); } } }