releases/tests/Paca.Tests/Core/DirectDownloaderIntegrityGateTests.cs

140 lines
5 KiB
C#

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<InvalidDataException>(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<HttpResponseMessage> 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);
}
}
}