using System; using System.IO; using System.Text; using System.Threading.Tasks; using Paca.Core.Config; using Paca.Core.Hls; using Xunit; namespace Paca.Tests.Core; public class PartialChunkIntegrityCacheGateTests : IDisposable { private readonly string _testDir; public PartialChunkIntegrityCacheGateTests() { _testDir = Path.Combine(Path.GetTempPath(), "paca_chunk_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 Gate457_PartialChunkIntegrityCache_ComputesSha256_Correctly() { var cache = new PartialChunkIntegrityCache(); var bytes = Encoding.UTF8.GetBytes("PACA-STREAM-CHUNK-INTEGRITY-2026"); var hash = cache.ComputeSha256(bytes); Assert.NotNull(hash); Assert.Equal(64, hash.Length); Assert.Matches("^[0-9a-f]{64}$", hash); } [Fact] public void Gate458_PartialChunkIntegrityCache_VerifiesIntactChunk_Correctly() { var cache = new PartialChunkIntegrityCache(); var chunkPath = Path.Combine(_testDir, "000001.seg"); var testData = Encoding.UTF8.GetBytes("VALID-CHUNK-DATA-123456789"); File.WriteAllBytes(chunkPath, testData); var expectedHash = cache.ComputeSha256(testData); var status = cache.VerifyChunkFile(chunkPath, expectedBytes: testData.Length, expectedSha256: expectedHash); Assert.Equal(ChunkIntegrityState.Verified, status); } [Fact] public void Gate459_PartialChunkIntegrityCache_DetectsZeroByteOrTruncatedChunk_AsCorrupt() { var cache = new PartialChunkIntegrityCache(); // 1. Zero-byte chunk var zeroPath = Path.Combine(_testDir, "zero.seg"); File.WriteAllBytes(zeroPath, Array.Empty()); var statusZero = cache.VerifyChunkFile(zeroPath, expectedBytes: 100); Assert.Equal(ChunkIntegrityState.Corrupt, statusZero); // 2. Truncated chunk (file is 10 bytes, expected 50) var truncPath = Path.Combine(_testDir, "trunc.seg"); File.WriteAllBytes(truncPath, new byte[10]); var statusTrunc = cache.VerifyChunkFile(truncPath, expectedBytes: 50); Assert.Equal(ChunkIntegrityState.Corrupt, statusTrunc); // 3. Missing chunk var missingPath = Path.Combine(_testDir, "nonexistent.seg"); var statusMissing = cache.VerifyChunkFile(missingPath); Assert.Equal(ChunkIntegrityState.Missing, statusMissing); } [Fact] public void Gate460_PartialChunkIntegrityCache_DetectsSha256Mismatch_AsCorrupt() { var cache = new PartialChunkIntegrityCache(); var chunkPath = Path.Combine(_testDir, "tampered.seg"); File.WriteAllBytes(chunkPath, Encoding.UTF8.GetBytes("Original Content")); var status = cache.VerifyChunkFile(chunkPath, expectedSha256: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"); Assert.Equal(ChunkIntegrityState.Corrupt, status); } [Fact] public void Gate461_PartialChunkIntegrityCache_PersistsAndRestores_LedgerJson() { var cache = new PartialChunkIntegrityCache(); var chunkName = "000000.seg"; var chunkPath = Path.Combine(_testDir, chunkName); var data = Encoding.UTF8.GetBytes("CHUNK-CONTENT-0"); File.WriteAllBytes(chunkPath, data); var hash = cache.ComputeSha256(data); cache.RecordChunk(_testDir, index: 0, chunkName: chunkName, bytes: data.Length, sha256: hash); cache.SaveLedger(_testDir); // New cache instance reading same directory var cache2 = new PartialChunkIntegrityCache(); cache2.LoadLedger(_testDir); var entry = cache2.GetChunk(_testDir, 0); Assert.NotNull(entry); Assert.Equal(0, entry!.Index); Assert.Equal(chunkName, entry.ChunkName); Assert.Equal(data.Length, entry.ActualBytes); Assert.Equal(hash, entry.Sha256Hash); Assert.Equal(ChunkIntegrityState.Verified, entry.State); } [Fact] public void Gate462_PartialChunkIntegrityCache_ReconcileAndClean_PurgesCorrupted_AndReturnsMissingIndices() { var cache = new PartialChunkIntegrityCache(); // Prepare 4 segments in _testDir: // seg 0: Valid, 20 bytes var seg0Path = Path.Combine(_testDir, "000000.seg"); var seg0Data = Encoding.UTF8.GetBytes("Valid 20-byte seg 0!"); File.WriteAllBytes(seg0Path, seg0Data); // seg 1: Corrupted (0 bytes) var seg1Path = Path.Combine(_testDir, "000001.seg"); File.WriteAllBytes(seg1Path, Array.Empty()); // seg 2: Missing (file doesn't exist) var seg2Path = Path.Combine(_testDir, "000002.seg"); // seg 3: Valid, 15 bytes var seg3Path = Path.Combine(_testDir, "000003.seg"); var seg3Data = Encoding.UTF8.GetBytes("Valid seg 3 data"); File.WriteAllBytes(seg3Path, seg3Data); // Reconcile 4 total chunks var report = cache.ReconcileAndClean(_testDir, totalSegments: 4, index => $"{index:000000}.seg"); Assert.Equal(4, report.TotalChunks); Assert.Equal(2, report.VerifiedCount); Assert.Equal(1, report.CorruptedOrTruncatedCount); Assert.Equal(1, report.MissingCount); Assert.False(report.IsFullyComplete); // Corrupted 0-byte file must have been purged from disk Assert.False(File.Exists(seg1Path)); // Valid files must remain Assert.True(File.Exists(seg0Path)); Assert.True(File.Exists(seg3Path)); // Segments needed for download are index 1 and 2 Assert.Equal(new[] { 1, 2 }, report.SegmentsToDownload); } [Fact] public void Gate463_HlsDownloader_IntegratesWithPartialChunkIntegrityCache_ExposesCache() { var config = new AppConfig(); var cache = new PartialChunkIntegrityCache(); using var http = new System.Net.Http.HttpClient(); var downloader = new HlsDownloader(http, config, Paca.Core.Platform.DesktopMediaMuxer.Default, null, cache); Assert.Same(cache, downloader.ChunkCache); } }