using System; using System.IO; using System.Linq; using System.Xml.Linq; using Xunit; namespace Paca.Tests.Mobile; /// /// 모바일 안드로이드 최신 버전 지원(minSdk 28+, targetSdk 36) 및 /// Obsolete API(DisplayAlert, Device.StartTimer, Frame 등) 퇴출 기계적 감사 게이트 /// public class MobileAndroidModernizationTests { private static string MobileDir => Path.Combine(SourceTree.Root, "src", "Paca.Mobile"); private static string AndroidManifestPath => Path.Combine(MobileDir, "Platforms", "Android", "AndroidManifest.xml"); private static string MobileCsprojPath => Path.Combine(MobileDir, "Paca.Mobile.csproj"); [Fact] public void Gate_Android_MinSdkVersion_Is_At_Least_28_And_Target_Is_36() { // 1. AndroidManifest.xml 검증 Assert.True(File.Exists(AndroidManifestPath), "AndroidManifest.xml 이 존재해야 합니다."); var manifestDoc = XDocument.Load(AndroidManifestPath); var usesSdk = manifestDoc.Descendants("uses-sdk").FirstOrDefault(); Assert.NotNull(usesSdk); XNamespace androidNs = "http://schemas.android.com/apk/res/android"; var minSdk = (int?)usesSdk.Attribute(androidNs + "minSdkVersion"); var targetSdk = (int?)usesSdk.Attribute(androidNs + "targetSdkVersion"); Assert.NotNull(minSdk); Assert.True(minSdk.Value >= 28, $"minSdkVersion은 최소 28(Android 9.0) 이상이어야 합니다. 현재: {minSdk.Value}"); Assert.NotNull(targetSdk); Assert.True(targetSdk.Value >= 36, $"targetSdkVersion은 최신 36(Android 16) 이상이어야 합니다. 현재: {targetSdk.Value}"); // 2. Paca.Mobile.csproj의 SupportedOSPlatformVersion 검증 Assert.True(File.Exists(MobileCsprojPath), "Paca.Mobile.csproj 가 존재해야 합니다."); var csprojContent = File.ReadAllText(MobileCsprojPath); Assert.DoesNotContain("24.0", csprojContent); Assert.Contains("28.0", csprojContent); } [Fact] public void Gate_Mobile_Packages_Are_Modernized() { var csprojContent = File.ReadAllText(MobileCsprojPath); // 1. ZXing.Net.Maui.Controls 최신화 및 넷6 구형 패키지(0.4.0) 퇴출 Assert.DoesNotContain("Include=\"ZXing.Net.Maui.Controls\" Version=\"0.4.0\"", csprojContent); Assert.Contains("Include=\"ZXing.Net.Maui.Controls\" Version=\"0.10.4\"", csprojContent); // 2. CommunityToolkit.Maui.MediaElement 최신화(.NET 10 대응 10.0.0) 및 4.0.0 퇴출 Assert.DoesNotContain("Include=\"CommunityToolkit.Maui.MediaElement\" Version=\"4.0.0\"", csprojContent); Assert.Contains("Include=\"CommunityToolkit.Maui.MediaElement\" Version=\"10.0.0\"", csprojContent); // 3. Plugin.LocalNotification 최신화 Assert.DoesNotContain("Include=\"Plugin.LocalNotification\" Version=\"11.1.0\"", csprojContent); Assert.Contains("Include=\"Plugin.LocalNotification\" Version=\"14.1.1\"", csprojContent); } [Fact] public void Gate_Mobile_No_Obsolete_Maui_Apis_Audit() { var csFiles = Directory.GetFiles(MobileDir, "*.cs", SearchOption.AllDirectories) .Where(f => !f.Contains("\\obj\\") && !f.Contains("/obj/") && !f.Contains("\\bin\\") && !f.Contains("/bin/")) .ToList(); foreach (var file in csFiles) { var fileName = Path.GetFileName(file); var lines = File.ReadAllLines(file); for (int i = 0; i < lines.Length; i++) { var line = lines[i]; var trimmed = line.Trim(); // 주석 라인은 제외 if (trimmed.StartsWith("//") || trimmed.StartsWith("/*") || trimmed.StartsWith("*")) continue; // 1. DisplayAlert( -> DisplayAlertAsync 사용 필수 if (line.Contains("DisplayAlert(") && !line.Contains("DisplayAlertAsync")) { Assert.Fail($"[{fileName}:{i + 1}] Obsolete API 'DisplayAlert' 가 발견되었습니다. 'DisplayAlertAsync'를 사용하세요: {trimmed}"); } // 2. DisplayActionSheet( -> DisplayActionSheetAsync 사용 필수 if (line.Contains("DisplayActionSheet(") && !line.Contains("DisplayActionSheetAsync")) { Assert.Fail($"[{fileName}:{i + 1}] Obsolete API 'DisplayActionSheet' 가 발견되었습니다. 'DisplayActionSheetAsync'를 사용하세요: {trimmed}"); } // 3. Device.StartTimer 퇴출 if (line.Contains("Device.StartTimer")) { Assert.Fail($"[{fileName}:{i + 1}] Obsolete API 'Device.StartTimer' 가 발견되었습니다. 'Dispatcher.StartTimer'를 사용하세요: {trimmed}"); } // 4. new Frame 퇴출 (Android FrameLayout 레이아웃 제외 MAUI Frame 컨트롤) if (line.Contains("new Frame") && !line.Contains("FrameLayout")) { Assert.Fail($"[{fileName}:{i + 1}] Obsolete API 'Frame' 이 발견되었습니다. 'Border'를 사용하세요: {trimmed}"); } // 5. MainPage 직접 탐색 퇴출 (Application.Current.MainPage) if (line.Contains(".MainPage")) { Assert.Fail($"[{fileName}:{i + 1}] Deprecated API 'MainPage' 가 발견되었습니다. Windows[0].Page 를 사용하세요: {trimmed}"); } } } } }