using System;
using System.IO;
using Paca.App.Browser.Features;
using Paca.Browser.Ads;
using Paca.Browser.NewTab;
using Paca.Tests.Infrastructure;
using Xunit;
namespace Paca.Tests.Browser;
///
/// 데스크톱 광고 슬롯 시스템, NTP 스폰서 카드 주입 및 배너 컨트롤 UI TDD 게이트 테스트 (Gate 197 ~ Gate 199)
///
public class DesktopAdSlotAndNtpSponsoredGateTests
{
private static string AppDir => Directory.Exists(Path.Combine(SourceTree.Root, "src", "Paca.App"))
? Path.Combine(SourceTree.Root, "src", "Paca.App")
: Path.Combine(SourceTree.Root, "Paca.App");
private static string BrowserDir => Directory.Exists(Path.Combine(SourceTree.Root, "src", "Paca.Browser"))
? Path.Combine(SourceTree.Root, "src", "Paca.Browser")
: Path.Combine(SourceTree.Root, "Paca.Browser");
[Fact]
public void Gate197_DesktopAdManager_Models_Placement_And_Dismissal_Audit()
{
var adManager = new DesktopAdManager();
// 1. 새 탭 페이지(NTP) 스폰서 슬롯 조회
var ntpAd = adManager.GetAdForPlacement(DesktopAdPlacement.NewTabPageSponsoredCard);
Assert.NotNull(ntpAd);
Assert.True(ntpAd.Badge == "AD" || ntpAd.Badge == "SPONSORED");
Assert.False(string.IsNullOrWhiteSpace(ntpAd.Title));
Assert.False(string.IsNullOrWhiteSpace(ntpAd.Advertiser));
Assert.False(string.IsNullOrWhiteSpace(ntpAd.TargetUrl));
Assert.True(ntpAd.IsVisible);
// 2. 다운로드 뷰 배너 슬롯 조회
var dlAd = adManager.GetAdForPlacement(DesktopAdPlacement.DownloadViewBanner);
Assert.NotNull(dlAd);
Assert.True(dlAd.IsVisible);
// 3. 인상(Impression) 및 클릭(Click) 트래킹
int initImpressions = adManager.GetImpressionCount(ntpAd.Id);
int initClicks = adManager.GetClickCount(ntpAd.Id);
adManager.TrackImpression(ntpAd.Id);
Assert.Equal(initImpressions + 1, adManager.GetImpressionCount(ntpAd.Id));
adManager.TrackClick(ntpAd.Id);
Assert.Equal(initClicks + 1, adManager.GetClickCount(ntpAd.Id));
// 4. 닫기(Dismiss) 및 가시성 제어
bool dismissedFired = false;
adManager.AdDismissed += id =>
{
if (id == ntpAd.Id) dismissedFired = true;
};
adManager.DismissAd(ntpAd.Id);
Assert.True(dismissedFired);
Assert.False(ntpAd.IsVisible);
// 닫힌 후 조회 시 null
var dismissedCheck = adManager.GetAdForPlacement(DesktopAdPlacement.NewTabPageSponsoredCard);
Assert.Null(dismissedCheck);
}
[Fact]
public void Gate198_NewTabPageBuilder_SponsoredCard_Markup_And_Accessibility_Audit()
{
// 1. NewTabPageBuilder에 스폰서 광고 데이터가 제공될 때 HTML 생성 검증
var adData = new DesktopAdData
{
Id = "test-desktop-sponsor",
Badge = "SPONSORED",
Title = "PACA Cloud Pro 10Gbps 가속",
Description = "초고속 클라우드 다운로드 릴레이 및 8K 인코딩",
Advertiser = "Paca Media Lab",
CtaText = "체험하기",
TargetUrl = "https://paca.cloud/pro",
IsVisible = true,
IsDismissible = true
};
var html = NewTabPageBuilder.BuildNtp(
shortcuts: null,
settings: null,
recentHistory: null,
recentDownloads: null,
isDark: true,
sponsoredAd: adData
);
// 2. 스폰서 컨테이너 및 속성 감사
Assert.Contains("ntp-sponsored-container", html);
Assert.Contains("data-sponsored-ad=\"true\"", html);
Assert.Contains("PACA Cloud Pro 10Gbps 가속", html);
Assert.Contains("Paca Media Lab", html);
Assert.Contains("https://paca.cloud/pro", html);
Assert.Contains("SPONSORED", html);
// 3. WCAG 2.2 접근성 계약: aria-label 및 dismiss 버튼
Assert.Contains("aria-label=\"스폰서 추천\"", html);
Assert.Contains("aria-label=\"광고 닫기\"", html);
// 4. 스폰서 광고가 비활성화(null)되었을 때는 렌더링되지 않아야 함
var cleanHtml = NewTabPageBuilder.BuildNtp(
shortcuts: null,
settings: null,
recentHistory: null,
recentDownloads: null,
isDark: true,
sponsoredAd: null
);
Assert.DoesNotContain("data-sponsored-ad=\"true\"", cleanHtml);
}
[Fact]
public void Gate199_DesktopAdBannerControl_Xaml_And_DesignSystem_Audit()
{
// 1. WPF DesktopAdBannerControl XAML 및 코드비하인드 파일 존재 확인
var xamlPath = Path.Combine(AppDir, "Controls", "DesktopAdBannerControl.xaml");
var csPath = Path.Combine(AppDir, "Controls", "DesktopAdBannerControl.xaml.cs");
Assert.True(File.Exists(xamlPath), "DesktopAdBannerControl.xaml 파일이 존재해야 합니다.");
Assert.True(File.Exists(csPath), "DesktopAdBannerControl.xaml.cs 파일이 존재해야 합니다.");
var xaml = File.ReadAllText(xamlPath);
var cs = File.ReadAllText(csPath);
// 2. Designpaca 25대 게이트 감사
// - 최소 32x32px 클릭 타깃 및 마진 8px 배수
Assert.Contains("Height=\"32\"", xaml);
// - AutomationProperties.Name 스크린리더 계약
Assert.Contains("AutomationProperties.Name", xaml);
// - 닫기 버튼 및 CTA 버튼
Assert.Contains("PART_DismissBtn", xaml);
Assert.Contains("PART_CtaBtn", xaml);
// - 폰트 및 라운드 코너
Assert.Contains("CornerRadius", xaml);
// 3. 코드비하인드에서 DesktopAdManager 연동 및 트래킹 호출 감사
Assert.Contains("TrackImpression", cs);
Assert.Contains("TrackClick", cs);
Assert.Contains("DismissAd", cs);
}
[Fact]
public void Gate202_DesktopAdConfig_AdMobUnitIds_And_EffectiveTestAd_Audit()
{
// 1. AdMob 콘솔에서 생성한 실제 배너 광고 단위 ID 일치성 단정
var config = DesktopAdConfig.PacaProduction;
Assert.Equal("ca-app-pub-1039714767792854~5680462775", config.AppId);
Assert.Equal("ca-app-pub-1039714767792854/1521297311", config.DownloadBannerAdUnitId);
Assert.Equal("ca-app-pub-1039714767792854/4387520844", config.NtpBannerAdUnitId);
// 2. 검토 대기(스토어 미연결) 상태 기본 테스트 모드: Google 공식 실시간 웹 디스플레이 테스트 슬롯 라우팅
Assert.True(config.ForceTestAds);
Assert.Equal(DesktopAdConfig.GoogleTestSlot, config.EffectiveDownloadBannerUnitId);
Assert.Equal(DesktopAdConfig.GoogleTestSlot, config.EffectiveNtpBannerUnitId);
// 3. 프로덕션 강제 지정 시 실제 콘솔 광고 단위 ID 반환
var prodConfig = config with { ForceTestAds = false };
Assert.Equal("ca-app-pub-1039714767792854/1521297311", prodConfig.EffectiveDownloadBannerUnitId);
Assert.Equal("ca-app-pub-1039714767792854/4387520844", prodConfig.EffectiveNtpBannerUnitId);
// 4. Google Publisher Tag (GPT) 스니펫 생성기 검증
var snippet = DesktopAdConfig.GenerateGoogleTagSnippet(DesktopAdConfig.GoogleTestSlot, 728, 90, "div-gpt-test");
Assert.Contains("securepubads.g.doubleclick.net/tag/js/gpt.js", snippet);
Assert.Contains($"googletag.defineSlot('{DesktopAdConfig.GoogleTestSlot}'", snippet);
Assert.Contains("[728, 90]", snippet);
Assert.Contains("googletag.display('div-gpt-test')", snippet);
// 5. DesktopAdData 실광고 속성 모델 검증
var ad = new DesktopAdData
{
AdUnitId = "ca-app-pub-1039714767792854/1521297311",
IsRealAd = true,
HtmlEmbedSnippet = snippet
};
Assert.True(ad.IsRealAd);
Assert.Equal("ca-app-pub-1039714767792854/1521297311", ad.AdUnitId);
Assert.NotNull(ad.HtmlEmbedSnippet);
}
[Fact]
public void Gate203_NewTabPageBuilder_RealAdUnit_GooglePublisherTag_Audit()
{
// Arrange: 실시간 구글 광고 태그가 포함된 DesktopAdData 구성
var realAd = new DesktopAdData
{
Id = "paca-desktop-ntp-real-ad",
AdUnitId = "ca-app-pub-1039714767792854/4387520844",
IsRealAd = true,
Badge = "AD",
Title = "Google Sponsor Network",
Advertiser = "Google Ad Manager",
HtmlEmbedSnippet = DesktopAdConfig.GenerateGoogleTagSnippet(DesktopAdConfig.GoogleTestSlot, 728, 90, "div-gpt-ad-ntp"),
IsVisible = true
};
// Act: NewTabPageBuilder HTML 생성
var html = NewTabPageBuilder.BuildNtp(
shortcuts: null,
settings: null,
recentHistory: null,
recentDownloads: null,
isDark: true,
sponsoredAd: realAd
);
// Assert: Google Publisher Tag 비동기 스크립트 및 실시간 광고 슬롯 컨테이너 마크업 검증
Assert.Contains("ntp-sponsored-container", html);
Assert.Contains("ntp-real-ad-container", html);
Assert.Contains("data-real-ad=\"true\"", html);
Assert.Contains("securepubads.g.doubleclick.net/tag/js/gpt.js", html);
Assert.Contains($"googletag.defineSlot('{DesktopAdConfig.GoogleTestSlot}'", html);
Assert.Contains("googletag.display('div-gpt-ad-ntp')", html);
}
[Fact]
public void Gate204_DesktopAdBannerControl_RealAdHost_And_WebView2_Audit()
{
var xamlPath = Path.Combine(AppDir, "Controls", "DesktopAdBannerControl.xaml");
var csPath = Path.Combine(AppDir, "Controls", "DesktopAdBannerControl.xaml.cs");
var xaml = File.ReadAllText(xamlPath);
var cs = File.ReadAllText(csPath);
// 1. 실광고 호스트 및 네이티브 카드 2단 계층 XAML 감사
Assert.Contains("PART_RealAdHost", xaml);
Assert.Contains("PART_NativeCard", xaml);
// 2. 코드비하인드에서 WebView2 환경 주입 및 실광고 내비게이션 지원 감사
Assert.Contains("AttachEnvironment", cs);
Assert.Contains("GenerateGoogleTagSnippet", cs);
}
[Fact]
public void Gate205_DownloadsPanelView_BottomAdBanner_And_RichImageCard_Audit()
{
// 1. MainWindow.xaml 다운로드 뷰 하단(Grid.Row=2) 배너 배치 감사
var mainWindowXamlPath = Path.Combine(AppDir, "MainWindow.xaml");
Assert.True(File.Exists(mainWindowXamlPath), "MainWindow.xaml 파일이 존재해야 합니다.");
var mainXaml = File.ReadAllText(mainWindowXamlPath);
// DownloadsPanelView 그리드 행 정의에 Row 2(Auto)가 추가되어 있어야 함
Assert.Contains(" 120 && c.G > 120 && c.B > 160)
{
textPixels++;
}
}
}
Assert.True(textPixels > 50, $"BottomAppBanner must contain visible text pixels in the center content area (Found {textPixels} text pixels)");
// 1. 다운로드 배너가 단색 빈 화면(762바이트)이 아니어야 함
var dlInfo = new FileInfo(dlFile);
Assert.True(dlInfo.Length > 2000, $"DownloadViewBanner preview must not be a blank solid rectangle (Length was {dlInfo.Length} bytes)");
}
[Fact]
public void Gate209_GoogleAds_AdsTxt_And_AppAdsTxt_Verification_Audit()
{
// 1. site/ads.txt 및 site/app-ads.txt 배포 파일 존재 및 내용 검증
var siteDir = Path.Combine(SourceTree.Root, "site");
var adsTxtPath = Path.Combine(siteDir, "ads.txt");
var appAdsTxtPath = Path.Combine(siteDir, "app-ads.txt");
Assert.True(File.Exists(adsTxtPath), "site/ads.txt 파일이 배포 루트에 존재해야 합니다.");
Assert.True(File.Exists(appAdsTxtPath), "site/app-ads.txt 파일이 배포 루트에 존재해야 합니다.");
var adsTxt = File.ReadAllText(adsTxtPath).Trim();
var appAdsTxt = File.ReadAllText(appAdsTxtPath).Trim();
// Google 공식 인증 레코드: google.com, pub-1039714767792854, DIRECT, f08c47fec0942fa0
const string expectedRecord = "google.com, pub-1039714767792854, DIRECT, f08c47fec0942fa0";
Assert.Contains(expectedRecord, adsTxt);
Assert.Contains(expectedRecord, appAdsTxt);
// 2. DesktopAdConfig 공식 퍼블리셔 ID 및 클라이언트 ID 단정
Assert.Equal("pub-1039714767792854", DesktopAdConfig.PublisherId);
Assert.Equal("ca-pub-1039714767792854", DesktopAdConfig.AdClient);
}
[Fact]
public void Gate210_ZeroMockAdvertising_And_RealGoogleOrigin_Audit()
{
// 1. DesktopAdModels.cs에 가짜 모의 광고(쿠팡, 삼성 등)가 완전히 제거되었는지 감사
var modelsCsPath = Path.Combine(BrowserDir, "Ads", "DesktopAdModels.cs");
Assert.True(File.Exists(modelsCsPath));
var modelsCs = File.ReadAllText(modelsCsPath);
Assert.DoesNotContain("쿠팡", modelsCs);
Assert.DoesNotContain("Coupang", modelsCs);
Assert.DoesNotContain("Galaxy S24", modelsCs);
Assert.DoesNotContain("삼성전자", modelsCs);
// 2. DesktopAdBannerControl.xaml.cs에서 가짜 목업이 아닌 실시간 Google 태그 호스팅 및 가상 HTTPS 오리진 감사
var bannerCsPath = Path.Combine(AppDir, "Controls", "DesktopAdBannerControl.xaml.cs");
Assert.True(File.Exists(bannerCsPath));
var bannerCs = File.ReadAllText(bannerCsPath);
Assert.Contains("ads.paca.cloud", bannerCs);
Assert.Contains("SetVirtualHostNameToFolderMapping", bannerCs);
Assert.Contains("ad-google-wrapper", bannerCs);
Assert.DoesNotContain("ad-logo-box", bannerCs);
Assert.DoesNotContain("ad-logo-fallback", bannerCs);
// 3. AdSense 스니펫 생성기 동작 검증
var adsenseSnippet = DesktopAdConfig.GenerateAdSenseSnippet(DesktopAdConfig.AdClient, "4387520844", 728, 90);
Assert.Contains("pagead2.googlesyndication.com/pagead/js/adsbygoogle.js", adsenseSnippet);
Assert.Contains("data-ad-client=\"ca-pub-1039714767792854\"", adsenseSnippet);
Assert.Contains("data-ad-slot=\"4387520844\"", adsenseSnippet);
}
[Fact]
public void Gate214_RealGoogleAds_AdSense_ProductionSnippets_And_DomainOrigin_Audit()
{
// 1. 프로덕션 모드 DesktopAdManager 생성 시 실제 단위 슬롯이 주입되어야 함
var prodConfig = DesktopAdConfig.PacaProduction with { ForceTestAds = false };
var adManager = new DesktopAdManager(prodConfig);
var bottomAd = adManager.GetAdForPlacement(DesktopAdPlacement.BottomAppBanner);
Assert.NotNull(bottomAd);
Assert.True(bottomAd.IsRealAd);
Assert.Equal("ca-app-pub-1039714767792854/4387520844", bottomAd.AdUnitId);
Assert.NotNull(bottomAd.HtmlEmbedSnippet);
Assert.Contains("ca-pub-1039714767792854", bottomAd.HtmlEmbedSnippet);
Assert.Contains("4387520844", bottomAd.HtmlEmbedSnippet);
Assert.Contains("pagead2.googlesyndication.com/pagead/js/adsbygoogle.js", bottomAd.HtmlEmbedSnippet);
var dlAd = adManager.GetAdForPlacement(DesktopAdPlacement.DownloadViewBanner);
Assert.NotNull(dlAd);
Assert.True(dlAd.IsRealAd);
Assert.Equal("ca-app-pub-1039714767792854/1521297311", dlAd.AdUnitId);
Assert.NotNull(dlAd.HtmlEmbedSnippet);
Assert.Contains("ca-pub-1039714767792854", dlAd.HtmlEmbedSnippet);
Assert.Contains("1521297311", dlAd.HtmlEmbedSnippet);
Assert.Contains("pagead2.googlesyndication.com/pagead/js/adsbygoogle.js", dlAd.HtmlEmbedSnippet);
// 2. DesktopAdConfig.GenerateProductionAdSnippet 검증
var snippet = DesktopAdConfig.GenerateProductionAdSnippet("ca-app-pub-1039714767792854/4387520844", 728, 90, "div-bottom");
Assert.Contains("ca-pub-1039714767792854", snippet);
Assert.Contains("data-ad-slot=\"4387520844\"", snippet);
Assert.Contains("data-ad-status", snippet);
// 3. DesktopAdBannerControl.xaml.cs에서 실 도메인 mediabrowser.chanpaca.net 및 크롬 User-Agent 감사
var bannerCsPath = Path.Combine(AppDir, "Controls", "DesktopAdBannerControl.xaml.cs");
var bannerCs = File.ReadAllText(bannerCsPath);
Assert.Contains("mediabrowser.chanpaca.net", bannerCs);
Assert.Contains("Chrome/130", bannerCs);
Assert.Contains("Settings.UserAgent", bannerCs);
}
}