releases/tests/Paca.Tests/Browser/DesktopAdSlotAndNtpSponsoredGateTests.cs

430 lines
19 KiB
C#

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;
/// <summary>
/// 데스크톱 광고 슬롯 시스템, NTP 스폰서 카드 주입 및 배너 컨트롤 UI TDD 게이트 테스트 (Gate 197 ~ Gate 199)
/// </summary>
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("<Grid x:Name=\"DownloadsPanelView\"", mainXaml);
Assert.Contains("x:Name=\"DownloadsAdBanner\" Grid.Row=\"2\"", mainXaml);
// 2. DesktopAdBannerControl.xaml 리치 이미지 썸네일 컨트롤 감사
var adBannerXamlPath = Path.Combine(AppDir, "Controls", "DesktopAdBannerControl.xaml");
var bannerXaml = File.ReadAllText(adBannerXamlPath);
Assert.Contains("PART_AdImage", bannerXaml);
// 3. DesktopAdBannerControl.xaml.cs 이미지 바인딩 및 표시 로직 감사
var adBannerCsPath = Path.Combine(AppDir, "Controls", "DesktopAdBannerControl.xaml.cs");
var bannerCs = File.ReadAllText(adBannerCsPath);
Assert.Contains("PART_AdImage", bannerCs);
Assert.Contains("IconOrImageUrl", bannerCs);
}
[Fact]
public void Gate206_DesktopAd_Vibrant_DisplayCreative_And_DirectCapture_Audit()
{
var adManager = new DesktopAdManager();
// 1. 하단 메인 배너(728x90) 실제 상용 수준 메타데이터 감사
var bottomAd = adManager.GetAdForPlacement(DesktopAdPlacement.BottomAppBanner);
Assert.NotNull(bottomAd);
Assert.True(bottomAd.IsRealAd);
Assert.False(string.IsNullOrWhiteSpace(bottomAd.Title));
Assert.False(string.IsNullOrWhiteSpace(bottomAd.Description), "하단 배너 설명문이 비어있어서는 안 됩니다.");
Assert.False(string.IsNullOrWhiteSpace(bottomAd.Advertiser));
Assert.False(string.IsNullOrWhiteSpace(bottomAd.IconOrImageUrl), "하단 배너 브랜드/아이콘 이미지가 비어있어서는 안 됩니다.");
Assert.False(string.IsNullOrWhiteSpace(bottomAd.CtaText));
// 2. 다운로드 뷰 배너(300x250) 메타데이터 감사
var dlAd = adManager.GetAdForPlacement(DesktopAdPlacement.DownloadViewBanner);
Assert.NotNull(dlAd);
Assert.True(dlAd.IsRealAd);
Assert.False(string.IsNullOrWhiteSpace(dlAd.Title));
Assert.False(string.IsNullOrWhiteSpace(dlAd.Description));
Assert.False(string.IsNullOrWhiteSpace(dlAd.Advertiser));
Assert.False(string.IsNullOrWhiteSpace(dlAd.IconOrImageUrl));
// 3. DesktopAdBannerControl.xaml.cs 직접 웹뷰 캡처 메서드 감사
var csPath = Path.Combine(AppDir, "Controls", "DesktopAdBannerControl.xaml.cs");
var cs = File.ReadAllText(csPath);
Assert.Contains("CaptureAdPreviewAsync", cs);
Assert.Contains("CapturePreviewAsync", cs);
}
[Fact]
public void Gate207_DesktopAd_Sizing_Encoding_And_OverlayRobustness_Audit()
{
// 1. DesktopAdBannerControl.xaml.cs 규격 및 인코딩, 타이밍 방어 감사
var adBannerCsPath = Path.Combine(AppDir, "Controls", "DesktopAdBannerControl.xaml.cs");
var cs = File.ReadAllText(adBannerCsPath);
// 728 및 300 명시적 Width 지정
Assert.Contains("Width = 728", cs);
Assert.Contains("Width = 300", cs);
// 웹뷰 자체 크기 명시적 지정
Assert.Contains("_webView.Width = adWidth", cs);
Assert.Contains("_webView.Height = adHeight", cs);
// UTF-8 BOM 인코딩 감사
Assert.Contains("UTF8Encoding(true)", cs);
// 캐시 버스팅 감사
Assert.Contains("v=", cs);
// 플렉스박스 붕괴 방지 min-width 및 margin-left: auto 감사
Assert.Contains("min-width: 320px", cs);
Assert.Contains("margin-left: auto", cs);
// 첫 프레임 페인트 대기 delay 감사
Assert.Contains("Task.Delay", cs);
// 2. AppDebugScreenshotService.cs 파일 락 방지 FileShare.ReadWrite 감사
var screenshotCsPath = Path.Combine(AppDir, "Services", "AppDebugScreenshotService.cs");
var screenshotCs = File.ReadAllText(screenshotCsPath);
Assert.Contains("FileShare.ReadWrite", screenshotCs);
}
[Fact]
public void Gate208_AdPreviewImages_MustContainVibrantRenderedPixels()
{
var previewsDir = Path.Combine(AppPaths.DataDir, "ad_previews");
var bottomFile = Path.Combine(previewsDir, "ad_BottomAppBanner.png");
var dlFile = Path.Combine(previewsDir, "ad_DownloadViewBanner.png");
if (!File.Exists(bottomFile) || !File.Exists(dlFile))
{
var canonicalDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Paca", "ad_previews");
if (File.Exists(Path.Combine(canonicalDir, "ad_BottomAppBanner.png")))
{
bottomFile = Path.Combine(canonicalDir, "ad_BottomAppBanner.png");
dlFile = Path.Combine(canonicalDir, "ad_DownloadViewBanner.png");
}
}
Assert.True(File.Exists(bottomFile), "Bottom banner preview file must exist");
Assert.True(File.Exists(dlFile), "Download banner preview file must exist");
using var bottomBmp = new System.Drawing.Bitmap(bottomFile);
using var dlBmp = new System.Drawing.Bitmap(dlFile);
// 2. 하단 배너 중앙 영역(x: 150~450)에 텍스트 픽셀(밝은 색상 텍스트)이 존재해야 함
int textPixels = 0;
for (int x = 150; x < 450; x++)
{
for (int y = 15; y < 75; y++)
{
var c = bottomBmp.GetPixel(x, y);
// 흰색/밝은 텍스트(#F8FAFC, #A5B4FC, #38BDF8) 픽셀 카운트
if (c.R > 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);
}
}