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,236 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Paca.Mobile.Core;
using Paca.Tests.Infrastructure;
using Xunit;
namespace Paca.Tests.Mobile;
/// <summary>
/// 모바일 AdMob 설정, 워터폴 폴백 및 보상형 광고 라이프사이클 TDD 게이트 테스트 (Gate 194 ~ Gate 196)
/// </summary>
public class MobileAdMobWaterfallAndRewardGateTests
{
private static string MobileCoreDir => Directory.Exists(Path.Combine(SourceTree.Root, "src", "Paca.Mobile.Core"))
? Path.Combine(SourceTree.Root, "src", "Paca.Mobile.Core")
: Path.Combine(SourceTree.Root, "Paca.Mobile.Core");
private static string MobileProjectDir => Directory.Exists(Path.Combine(SourceTree.Root, "src", "Paca.Mobile"))
? Path.Combine(SourceTree.Root, "src", "Paca.Mobile")
: Path.Combine(SourceTree.Root, "Paca.Mobile");
[Fact]
public void Gate194_AdMobConfig_GoogleOfficialTestIds_And_Validation_Audit()
{
// 1. Google 공식 Android AdMob Test IDs 기본값 검증
var config = AdMobConfig.DefaultTestConfig;
Assert.Equal("ca-app-pub-3940256099942544~3347511713", config.AppId);
Assert.Equal("ca-app-pub-3940256099942544/6300978111", config.BannerAdUnitId);
Assert.Equal("ca-app-pub-3940256099942544/1033173712", config.InterstitialAdUnitId);
Assert.Equal("ca-app-pub-3940256099942544/5224354917", config.RewardedAdUnitId);
Assert.True(config.IsTestMode);
// 2. 커스텀 프로덕션 ID 주입 및 유효성 검증
var prodConfig = new AdMobConfig(
appId: "ca-app-pub-1234567890123456~1234567890",
bannerAdUnitId: "ca-app-pub-1234567890123456/1111111111",
interstitialAdUnitId: "ca-app-pub-1234567890123456/2222222222",
rewardedAdUnitId: "ca-app-pub-1234567890123456/3333333333",
isTestMode: false
);
Assert.False(prodConfig.IsTestMode);
Assert.True(prodConfig.IsValid());
// 3. 잘못된 형식(유효하지 않은 접두사) 검증 방어
var invalidConfig = new AdMobConfig(
appId: "invalid-app-id",
bannerAdUnitId: "",
interstitialAdUnitId: "",
rewardedAdUnitId: "",
isTestMode: false
);
Assert.False(invalidConfig.IsValid());
}
[Fact]
public async Task Gate195_AdMobWaterfall_SuccessAndFallbackLifecycle_Audit()
{
var localAdManager = new MobileAdManager();
var fallbackAd = localAdManager.GetAdForPlacement(MobileAdPlacement.NewTabPageSponsoredCard);
Assert.NotNull(fallbackAd);
// 1. 성공 시나리오 Mock 어댑터
var successAdapter = new MockAdMobAdapter(shouldSucceed: true, adUnitId: "test-banner-unit");
var controller = new AdMobWaterfallController(successAdapter, localAdManager);
var result = await controller.RequestBannerAdAsync(MobileAdPlacement.NewTabPageSponsoredCard);
Assert.NotNull(result);
Assert.Equal(AdSourceType.GoogleAdMob, result.SourceType);
Assert.Equal("test-banner-unit", result.AdMobUnitId);
Assert.True(result.IsAdMobActive);
// 인상 추적 확인
controller.TrackImpression(result);
Assert.Equal(1, successAdapter.ImpressionCount);
// 2. 실패 시나리오 (No Fill / Offline) -> 자체 스폰서 배너로 자동 Waterfall Fallback
var failureAdapter = new MockAdMobAdapter(shouldSucceed: false, adUnitId: "failing-unit");
var fallbackController = new AdMobWaterfallController(failureAdapter, localAdManager);
var fallbackResult = await fallbackController.RequestBannerAdAsync(MobileAdPlacement.NewTabPageSponsoredCard);
Assert.NotNull(fallbackResult);
Assert.Equal(AdSourceType.DirectSponsorFallback, fallbackResult.SourceType);
Assert.False(fallbackResult.IsAdMobActive);
Assert.NotNull(fallbackResult.FallbackDirectAd);
Assert.Equal(fallbackAd.Id, fallbackResult.FallbackDirectAd.Id);
// Fallback 인상 추적 확인
fallbackController.TrackImpression(fallbackResult);
Assert.Equal(0, failureAdapter.ImpressionCount);
Assert.True(localAdManager.GetImpressionCount(fallbackAd.Id) > 0);
}
[Fact]
public async Task Gate196_AdMobRewardedAd_BoostToken_Lifecycle_Audit()
{
var rewardManager = new RewardedBoostTokenManager();
Assert.False(rewardManager.IsBoostActive);
var mockAdapter = new MockAdMobAdapter(shouldSucceed: true, adUnitId: "rewarded-unit");
// 1. 보상형 광고 시청 완료 콜백 시뮬레이션
bool rewardTriggered = false;
mockAdapter.OnUserEarnedReward += (type, amount) =>
{
rewardTriggered = true;
rewardManager.GrantBoost(TimeSpan.FromHours(1));
};
var showResult = await mockAdapter.ShowRewardedAdAsync();
Assert.True(showResult);
Assert.True(rewardTriggered);
// 2. 부스트 토큰 활성화 확인
Assert.True(rewardManager.IsBoostActive);
Assert.True(rewardManager.RemainingBoostTime.TotalMinutes > 50);
// 3. 만료 시뮬레이션
rewardManager.RevokeBoost();
Assert.False(rewardManager.IsBoostActive);
}
[Fact]
public void Gate197_AdMobConfig_PacaProduction_RealIds_And_ResolveByBuild_Audit()
{
// 1. PACA 프로덕션 설정 실제 ID 검증
var prod = AdMobConfig.PacaProduction;
Assert.Equal("ca-app-pub-1039714767792854~5680462775", prod.AppId);
Assert.Equal("ca-app-pub-1039714767792854/3110491400", prod.BannerAdUnitId);
Assert.False(prod.IsTestMode);
Assert.True(prod.IsValid());
// 2. 환경변수 미설정 시 빌드 구성에 따른 해석 (ADMOB_APP_ID / ADMOB_BANNER_ID 백업 후 제거)
var backupAppId = Environment.GetEnvironmentVariable("ADMOB_APP_ID");
var backupBannerId = Environment.GetEnvironmentVariable("ADMOB_BANNER_ID");
try
{
Environment.SetEnvironmentVariable("ADMOB_APP_ID", null);
Environment.SetEnvironmentVariable("ADMOB_BANNER_ID", null);
var debugResolved = AdMobConfig.Resolve(isDebugBuild: true);
Assert.Equal(AdMobConfig.DefaultTestConfig.AppId, debugResolved.AppId);
Assert.Equal(AdMobConfig.DefaultTestConfig.BannerAdUnitId, debugResolved.BannerAdUnitId);
Assert.True(debugResolved.IsTestMode);
var releaseResolved = AdMobConfig.Resolve(isDebugBuild: false);
Assert.Equal(AdMobConfig.PacaProduction.AppId, releaseResolved.AppId);
Assert.Equal(AdMobConfig.PacaProduction.BannerAdUnitId, releaseResolved.BannerAdUnitId);
Assert.False(releaseResolved.IsTestMode);
// 3. 환경변수가 설정되어 있으면 빌드 구성과 무관하게 환경변수 값을 우선한다
Environment.SetEnvironmentVariable("ADMOB_APP_ID", "ca-app-pub-9999999999999999~1111111111");
Environment.SetEnvironmentVariable("ADMOB_BANNER_ID", "ca-app-pub-9999999999999999/2222222222");
var envDebugResolved = AdMobConfig.Resolve(isDebugBuild: true);
Assert.Equal("ca-app-pub-9999999999999999~1111111111", envDebugResolved.AppId);
Assert.Equal("ca-app-pub-9999999999999999/2222222222", envDebugResolved.BannerAdUnitId);
var envReleaseResolved = AdMobConfig.Resolve(isDebugBuild: false);
Assert.Equal("ca-app-pub-9999999999999999~1111111111", envReleaseResolved.AppId);
Assert.Equal("ca-app-pub-9999999999999999/2222222222", envReleaseResolved.BannerAdUnitId);
}
finally
{
Environment.SetEnvironmentVariable("ADMOB_APP_ID", backupAppId);
Environment.SetEnvironmentVariable("ADMOB_BANNER_ID", backupBannerId);
}
}
[Fact]
public void Gate198_AndroidManifest_Uses_PacaProduction_AdMobAppId_Audit()
{
var manifestPath = Path.Combine(MobileProjectDir, "Platforms", "Android", "AndroidManifest.xml");
var manifestXml = File.ReadAllText(manifestPath);
Assert.Contains(
$"android:value=\"{AdMobConfig.PacaProduction.AppId}\"",
manifestXml);
Assert.DoesNotContain("ca-app-pub-3940256099942544~3347511713", manifestXml);
}
[Fact]
public void Gate199_PacaMobile_Csproj_References_GooglePlayServicesAds_Audit()
{
var csprojPath = Path.Combine(MobileProjectDir, "Paca.Mobile.csproj");
var csprojXml = File.ReadAllText(csprojPath);
Assert.Contains("Xamarin.GooglePlayServices.Ads", csprojXml);
}
[Fact]
public void Gate200_AdMobConfig_ForceRealTestAds_WhenUnderReview_Audit()
{
// 1. 기본 테스트 설정 및 프로덕션 설정에서 EffectiveBannerAdUnitId 검증
var testConfig = AdMobConfig.DefaultTestConfig;
Assert.Equal("ca-app-pub-3940256099942544/6300978111", testConfig.EffectiveBannerAdUnitId);
Assert.True(testConfig.IsTestMode);
// 2. 프로덕션 설정이라도 forceTestAds 가 활성화되면 실제 Google 공식 테스트 배너 ID 반환
var prodUnderReview = AdMobConfig.PacaProduction with { ForceTestAds = true };
Assert.True(prodUnderReview.ForceTestAds);
Assert.Equal("ca-app-pub-3940256099942544/6300978111", prodUnderReview.EffectiveBannerAdUnitId);
// 3. Resolve 메서드에서 검토 필요 상태를 위한 forceTestAds 기본 동작 검증
var backupForce = Environment.GetEnvironmentVariable("ADMOB_FORCE_TEST_ADS");
try
{
Environment.SetEnvironmentVariable("ADMOB_FORCE_TEST_ADS", "true");
var resolvedWithForce = AdMobConfig.Resolve(isDebugBuild: false);
Assert.Equal("ca-app-pub-3940256099942544/6300978111", resolvedWithForce.EffectiveBannerAdUnitId);
Assert.True(resolvedWithForce.ForceTestAds);
}
finally
{
Environment.SetEnvironmentVariable("ADMOB_FORCE_TEST_ADS", backupForce);
}
}
[Fact]
public void Gate201_AndroidAdMobAdapter_RequestConfiguration_And_TestDevice_Audit()
{
var adapterPath = Path.Combine(MobileProjectDir, "Platforms", "Android", "AndroidAdMobAdapter.cs");
var adapterCode = File.ReadAllText(adapterPath);
// Google Mobile Ads SDK 의 RequestConfiguration 설정 및 테스트 디바이스 등록 보증
Assert.Contains("RequestConfiguration", adapterCode);
Assert.Contains("SetTestDeviceIds", adapterCode);
Assert.Contains("DeviceIdEmulator", adapterCode);
Assert.Contains("EffectiveBannerAdUnitId", adapterCode);
}
}