feat(ads/seo/release): v1.2.0 with live Google Ads, isolated ad profile, ads.txt, and SEO/AEO/GEO optimization

This commit is contained in:
Yun Chan 2026-09-14 00:39:44 +09:00
parent a88aafa1e5
commit 0e568f1f0a
975 changed files with 130593 additions and 16783 deletions

View file

@ -0,0 +1,127 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Paca.Mobile.Core;
using Xunit;
namespace Paca.Tests.Mobile;
/// <summary>
/// Gate 167 ~ 170: PacaDrop 다중 미디어 배치 번들링 & 자동 스트리밍 언패커 기계적 검증
/// </summary>
public class MobilePacaDropBatchBundleGateTests
{
[Fact]
public void Gate167_BatchBundleManifest_Integrity()
{
var file1Data = Encoding.UTF8.GetBytes("Media Content 1 - 4K Video Stream");
var file2Data = Encoding.UTF8.GetBytes("Media Content 2 - 128k Audio Track");
var entries = new List<BundleEntryMetadata>
{
new("video1.mp4", file1Data.Length, Convert.ToHexString(SHA256.HashData(file1Data))),
new("audio1.mp3", file2Data.Length, Convert.ToHexString(SHA256.HashData(file2Data)))
};
var manifest = new BatchBundleManifest("bundle-001", entries, DateTimeOffset.UtcNow);
Assert.Equal("bundle-001", manifest.BundleId);
Assert.Equal(2, manifest.Entries.Count);
Assert.Equal(file1Data.Length + file2Data.Length, manifest.TotalBytes);
Assert.True(manifest.VerifyEntry("video1.mp4", file1Data));
Assert.False(manifest.VerifyEntry("video1.mp4", Encoding.UTF8.GetBytes("Corrupted")));
}
[Fact]
public async Task Gate168_BatchBundle_Pack_And_Unpack_Streaming()
{
var file1Data = Encoding.UTF8.GetBytes("Payload for Track 1");
var file2Data = Encoding.UTF8.GetBytes("Payload for Track 2");
using var bundleStream = new MemoryStream();
// 1. 번들 생성 (PC 측 동작 시뮬레이션)
await PacaBatchBundlePacker.PackAsync(bundleStream, new Dictionary<string, byte[]>
{
["intro.mp4"] = file1Data,
["outro.mp3"] = file2Data
});
bundleStream.Position = 0;
// 2. 모바일 언패커로 추출 (모바일 수신 시뮬레이션)
var tempExtractDir = Path.Combine(Path.GetTempPath(), "Paca_Test_Bundle_" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempExtractDir);
try
{
var extractedFiles = await PacaBatchBundleExtractor.ExtractAsync(bundleStream, tempExtractDir);
Assert.Equal(2, extractedFiles.Count);
Assert.True(File.Exists(Path.Combine(tempExtractDir, "intro.mp4")));
Assert.True(File.Exists(Path.Combine(tempExtractDir, "outro.mp3")));
var read1 = await File.ReadAllBytesAsync(Path.Combine(tempExtractDir, "intro.mp4"));
var read2 = await File.ReadAllBytesAsync(Path.Combine(tempExtractDir, "outro.mp3"));
Assert.Equal(file1Data, read1);
Assert.Equal(file2Data, read2);
}
finally
{
if (Directory.Exists(tempExtractDir))
{
Directory.Delete(tempExtractDir, recursive: true);
}
}
}
[Fact]
public async Task Gate169_BatchBundle_CorruptedEntry_FailsSafely()
{
using var garbageStream = new MemoryStream(Encoding.UTF8.GetBytes("Not A Valid Paca Bundle Data"));
var tempExtractDir = Path.Combine(Path.GetTempPath(), "Paca_Corrupt_" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempExtractDir);
try
{
await Assert.ThrowsAnyAsync<Exception>(async () =>
{
await PacaBatchBundleExtractor.ExtractAsync(garbageStream, tempExtractDir);
});
}
finally
{
if (Directory.Exists(tempExtractDir)) Directory.Delete(tempExtractDir, recursive: true);
}
}
[Fact]
public void Gate170_AutoIngestToLibraryManager()
{
var manager = new OfflineLibraryManager();
var extractedEntries = new List<BundleExtractedFile>
{
new("item-1", "video.mp4", 50_000_000, "/temp/video.mp4", true),
new("item-2", "song.mp3", 5_000_000, "/temp/song.mp3", true)
};
foreach (var entry in extractedEntries)
{
manager.RegisterMedia(new OfflineMediaItem(
entry.Id,
entry.FileName,
entry.SizeBytes,
entry.FilePath,
DateTimeOffset.UtcNow,
entry.IsChecksumVerified
));
}
Assert.Equal(55_000_000L, manager.TotalPacaBytes);
Assert.Equal(2, manager.GetItems(MediaFilter.All, MediaSortOrder.NewestFirst).Count);
}
}