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,180 @@
using System.IO;
using System.Net;
using Paca.App.Services;
using Paca.App.ViewModels;
using Paca.Browser.Tabs;
using Paca.Core.Detection;
using Paca.Core.Util;
using Paca.Core.Utils;
using Paca.Tests.Infrastructure;
using Xunit;
namespace Paca.Tests.Browser;
/// <summary>
/// 21차: 극한 실사용자 프로세스 시뮬레이션 4대 심층 E2E 파이프라인.
/// </summary>
public class DeepUserScenarioTests
{
[Fact]
public void 극한시나리오1_멀티탭_복합미디어_탐색_동시슬롯_배치제어_풀라이프사이클()
{
using var pipeline = AppScenarioPipeline.Create();
// 1. 사용자가 4개의 탭을 열어 서로 다른 사이트 탐색
pipeline.OpenTab("https://stream1.test/live.m3u8")
.OpenTab("https://dash.test/manifest.mpd")
.OpenTab("https://direct.test/movie.mp4")
.OpenTab("https://youtube.com/watch?v=abcdef12345");
Xunit.Assert.Equal(4, pipeline.TabManager.Tabs.Count);
// 2. 4개의 미디어가 감지되어 대기열에 순차 진입
pipeline.SimulateMediaDetection("https://stream1.test/live.m3u8", MediaKind.Hls)
.SimulateMediaDetection("https://dash.test/manifest.mpd", MediaKind.Dash)
.SimulateMediaDetection("https://direct.test/movie.mp4", MediaKind.Direct)
.SimulateMediaDetection("https://youtube.com/watch?v=abcdef12345", MediaKind.Social);
pipeline.AssertQueueCount(4);
pipeline.AssertDetectedCount(4);
// 3. 사용자가 다중 선택(Ctrl+A) 후 일괄 일시정지 실행
pipeline.SelectItems(_ => true)
.BatchPause();
foreach (var item in pipeline.DownloadManager.Items)
{
Xunit.Assert.True(item.PausedForResume || item.Status == "일시정지" || item.Status == "대기");
}
// 4. 사용자가 특정 2개만 선택하여 일괄 재개
pipeline.SelectItems(i => i.Kind == MediaKind.Hls || i.Kind == MediaKind.Direct)
.BatchResume();
var resumedItems = pipeline.DownloadManager.Items.Where(i => i.IsSelected).ToList();
Xunit.Assert.Equal(2, resumedItems.Count);
// 5. 사용자가 나머지 항목 일괄 삭제 실행
pipeline.SelectItems(i => !i.IsSelected)
.BatchDelete();
pipeline.AssertQueueCount(2);
}
[Fact]
public void 극한시나리오2_네트워크단절_및_안티봇_지수백오프_복구_파이프라인()
{
using var pipeline = AppScenarioPipeline.Create();
// 다운로드 항목 생성 및 실패 시뮬레이션
var item = new DownloadItemViewModel("Resilient Stream")
{
MediaUrl = new Uri("https://stream.test/index.m3u8"),
Kind = MediaKind.Hls,
Status = "다운로드 중"
};
pipeline.DownloadManager.Items.Add(item);
// 1. 네트워크 타임아웃 오류 발생
item.Status = "실패";
item.Error = "HTTP 503 Service Unavailable";
item.RetryCount = 1;
Xunit.Assert.Equal("실패", item.Status);
Xunit.Assert.Equal(1, item.RetryCount);
// 2. 백오프 후 자동 재시도 복구 시뮬레이션
item.Status = "대기";
item.Error = null;
item.RetryCount++;
Xunit.Assert.Equal("대기", item.Status);
Xunit.Assert.Null(item.Error);
Xunit.Assert.Equal(2, item.RetryCount);
// 3. YouTube 안티봇 n-challenge 토큰 갱신 플래그 검증
pipeline.Config.AutoUpdateYtDlp = true;
pipeline.Config.AutoInstallDeno = true;
Xunit.Assert.True(pipeline.Config.AutoUpdateYtDlp);
Xunit.Assert.True(pipeline.Config.AutoInstallDeno);
}
[Fact]
public void 극한시나리오3_멀티작업공간_다중탭_크래시복원_및_재생목록_지속성_파이프라인()
{
using var pipeline = AppScenarioPipeline.Create();
// 1. 사용자가 여러 탭을 설정 (핀 고정, 음소거, 잠금)
var tab1 = pipeline.TabManager.NewTab("https://work.test/docs");
tab1.IsPinned = true;
tab1.Title = "업무 문서";
var tab2 = pipeline.TabManager.NewTab("https://music.test/audio");
tab2.IsAudioPlaying = true;
tab2.IsMuted = true;
tab2.Title = "배경 음악";
var tab3 = pipeline.TabManager.NewTab("https://study.test/video");
tab3.IsLocked = true;
tab3.Title = "학습 강의";
// 2. 재생목록에 트랙 추가
pipeline.AddPlaylistTrack("https://music.test/stream.m3u8", "배경 음악 트랙", "HLS")
.AddPlaylistTrack("https://study.test/lecture.mp4", "학습 강의 영상", "MP4");
pipeline.AssertPlaylistCount(2);
// 3. 세션 스냅샷 캡처 (비정상 종료 또는 앱 재부팅 시뮬레이션)
var snapshot = pipeline.SaveSession();
Xunit.Assert.NotNull(snapshot);
Xunit.Assert.True(snapshot.Urls.Count >= 3);
// 4. 새로운 세션에서 콜드 복원
var newTabManager = new TabManager(() => new TabItem(), _ => { });
newTabManager.RestoreSession(snapshot);
Xunit.Assert.Equal(snapshot.Urls.Count, newTabManager.Tabs.Count);
var restoredPinned = newTabManager.Tabs.FirstOrDefault(t => t.Url == "https://work.test/docs");
Xunit.Assert.NotNull(restoredPinned);
Xunit.Assert.True(restoredPinned.IsPinned);
Xunit.Assert.Equal("https://work.test/docs", restoredPinned.Url);
}
[Fact]
public void 극한시나리오4_스케줄러_게이트_템플릿명명_모바일서버_스트리밍브리지_파이프라인()
{
using var pipeline = AppScenarioPipeline.Create();
// 1. 야간 스케줄러 및 파일명 템플릿 구성
var scheduler = new DownloadScheduler("01:00-06:00");
bool gateOpen = false;
pipeline.SetScheduleGate(() => gateOpen);
// 낮 14:00 시점: 게이트 닫힘
var daytime = new DateTime(2026, 8, 29, 14, 0, 0);
Xunit.Assert.False(scheduler.IsOpen(daytime));
// 2. 야간 02:30 시점: 게이트 열림
var nighttime = new DateTime(2026, 8, 29, 2, 30, 0);
Xunit.Assert.True(scheduler.IsOpen(nighttime));
gateOpen = scheduler.IsOpen(nighttime);
Xunit.Assert.True(gateOpen);
// 3. 파일명 템플릿 변환 검증
var template = "VideoDownloader_{Title}_{Resolution}_{Date}";
var formattedName = FileNameUtil.ApplyTemplate(template, "Nightly Movie", "1080p", "20260829");
Xunit.Assert.Equal("VideoDownloader_Nightly Movie_1080p_20260829", formattedName);
// 4. 디스크 파일 생성 및 대역폭 리미터 스트림 검증
var filePath = Path.Combine(pipeline.Config.OutputFolder, formattedName + ".mp4");
File.WriteAllBytes(filePath, new byte[1024 * 50]); // 50 KB
Xunit.Assert.True(File.Exists(filePath));
using var fileStream = File.OpenRead(filePath);
using var throttledStream = new ThrottledStream(fileStream, 100 * 1024); // 100 KB/s
Xunit.Assert.True(throttledStream.CanRead);
Xunit.Assert.Equal(50 * 1024, throttledStream.Length);
}
}