using System; using System.IO; using System.Linq; using System.Threading.Tasks; using System.Windows; using Paca.App; using Paca.App.Browser.Features; using Paca.Browser; using Paca.Browser.Tabs; using Xunit; namespace Paca.Tests.Browser; /// /// 사생활 보호(InPrivate/Private Browsing) 무결성 검증 게이트: /// 세션 스냅샷, 닫힌 탭 복원, 방문 기록 영속화, 원시 네트워크 로그, NTP 대시보드 무누수 검증. /// public class PrivateBrowsingSessionGateTests { private static TabManager NewManager() => new(() => new TabItem()); [Fact] public void CaptureSession_사생활보호_InPrivate_탭은_세션스냅샷에_포함되지_않는다() { var m = NewManager(); var normalTab = m.NewTab("https://public-site.com"); var privateTab = m.NewTab("https://secret-bank.com", isPrivate: true); m.Activate(privateTab); var snap = m.CaptureSession(); Assert.Contains("https://public-site.com", snap.Urls); Assert.DoesNotContain("https://secret-bank.com", snap.Urls); Assert.Single(snap.Urls); } [Fact] public void CaptureSession_모든_탭이_InPrivate일때_비공개URL이_누출되지_않는다() { var m = NewManager(); m.NewTab("https://confidential-1.com", isPrivate: true); m.NewTab("https://confidential-2.com", isPrivate: true); var snap = m.CaptureSession(); Assert.DoesNotContain("https://confidential-1.com", snap.Urls); Assert.DoesNotContain("https://confidential-2.com", snap.Urls); } [Fact] public void ReopenClosedTab_InPrivate_탭_복원_시_IsPrivate_속성이_그대로_보존된다() { var m = NewManager(); var privateTab = m.NewTab("https://secret-passwords.com", isPrivate: true); // InPrivate 탭 닫기 m.CloseTab(privateTab); // Ctrl+Shift+T 복원 시 InPrivate 상태가 유지되어야 함 (일반 탭으로 격하 누수 방지) var reopened = m.ReopenClosedTab(); Assert.NotNull(reopened); Assert.True(reopened!.IsPrivate, "복원된 탭도 IsPrivate 상태가 엄격히 유지되어야 합니다."); Assert.Equal("https://secret-passwords.com", reopened.Url); } [Fact] public void NewTabPageBuilder_InPrivate_NTP는_안내문구와_검색창을_제공하고_일반기록은_배제한다() { var html = NewTabPageBuilder.BuildInPrivateNtp(isDark: true, accentColor: "#8B5CF6"); Assert.NotNull(html); Assert.Contains("InPrivate", html, StringComparison.OrdinalIgnoreCase); Assert.Contains("방문 기록", html); Assert.Contains("쿠키", html); Assert.Contains("저장되지 않습니다", html); // 일반 NTP의 컴포넌트(shortcuts, recentHistory, recentDownloads) JSON 데이터가 없어야 함 Assert.DoesNotContain("\"recentHistory\":[", html); Assert.DoesNotContain("\"recentDownloads\":[", html); } [Fact] public void NewTabPageBuilder_IsNewTabUrl_paca_inprivate_인식_검증() { Assert.True(NewTabPageBuilder.IsNewTabUrl("paca://inprivate")); Assert.True(NewTabPageBuilder.IsNewTabUrl("about:inprivate")); } [Fact] public async Task MainWindow_InPrivate_창_생성_시_기본_IsPrivate_및_비공개_식별자_유지() { var dataDir = Path.Combine(Path.GetTempPath(), $"paca_inprivate_test_{Guid.NewGuid():N}"); Environment.SetEnvironmentVariable("VD_DATA_DIR", dataDir); try { using var sta = StaDispatcher.Start(); WpfTestApp.Ensure(sta.Dispatcher); await sta.InvokeAsync(async () => { var privateWindow = new MainWindow(isPrivateWindow: true); privateWindow.Show(); Assert.True(privateWindow.IsPrivateWindow, "isPrivateWindow: true로 생성된 창의 IsPrivateWindow는 true여야 합니다."); Assert.Contains("InPrivate", privateWindow.Title, StringComparison.OrdinalIgnoreCase); // 새 탭 생성 시 기본으로 InPrivate인지 확인 var newTab = privateWindow.Tabs.NewTab("https://test.org"); Assert.True(newTab.IsPrivate, "InPrivate 창에서 생성된 탭은 기본적으로 IsPrivate이어야 합니다."); privateWindow.Close(); await Task.CompletedTask; }); } finally { try { if (Directory.Exists(dataDir)) Directory.Delete(dataDir, true); } catch { } } } [Fact] public async Task InPrivate_탭_종료_시_아카이브_파일_archive_json에_저장되지_않는다() { var dataDir = Path.Combine(Path.GetTempPath(), $"paca_archive_leak_test_{Guid.NewGuid():N}"); Environment.SetEnvironmentVariable("VD_DATA_DIR", dataDir); try { using var sta = StaDispatcher.Start(); WpfTestApp.Ensure(sta.Dispatcher); await sta.InvokeAsync(async () => { var window = new MainWindow(); window.Show(); // 1. InPrivate 탭 생성 및 닫기 var secretTab = window.Tabs.NewTab("https://classified-credentials.com", isPrivate: true); secretTab.Title = "기밀 페이지"; window.Tabs.CloseTab(secretTab); // 2. archive.json 확인: InPrivate 탭이 저장되지 않았어야 함 var archivePath = Path.Combine(dataDir, "archive.json"); if (File.Exists(archivePath)) { var archiveContent = File.ReadAllText(archivePath); Assert.DoesNotContain("classified-credentials.com", archiveContent); } // 3. 일반 탭 생성 및 닫기 -> 일반 탭은 정상 저장되어야 함 var normalTab = window.Tabs.NewTab("https://public-news.com"); normalTab.Title = "공개 뉴스"; window.Tabs.CloseTab(normalTab); if (File.Exists(archivePath)) { var archiveContent = File.ReadAllText(archivePath); Assert.Contains("public-news.com", archiveContent); } window.Close(); await Task.CompletedTask; }); } finally { try { if (Directory.Exists(dataDir)) Directory.Delete(dataDir, true); } catch { } } } [Fact] public async Task InPrivate_탭_활성화_시_주소창_InPrivate_배지가_동기화된다() { var dataDir = Path.Combine(Path.GetTempPath(), $"paca_badge_test_{Guid.NewGuid():N}"); Environment.SetEnvironmentVariable("VD_DATA_DIR", dataDir); try { using var sta = StaDispatcher.Start(); WpfTestApp.Ensure(sta.Dispatcher); await sta.InvokeAsync(async () => { var window = new MainWindow(); window.Show(); var normalTab = window.Tabs.NewTab("https://normal.com", activate: true); Assert.Equal(Visibility.Collapsed, window.InPrivateAddressBadge.Visibility); var privateTab = window.Tabs.NewTab("https://secret.com", activate: true, isPrivate: true); Assert.Equal(Visibility.Visible, window.InPrivateAddressBadge.Visibility); window.Tabs.Activate(normalTab); Assert.Equal(Visibility.Collapsed, window.InPrivateAddressBadge.Visibility); window.Close(); await Task.CompletedTask; }); } finally { try { if (Directory.Exists(dataDir)) Directory.Delete(dataDir, true); } catch { } } } }