using System; using System.Collections.Generic; using System.IO; using System.Linq; using Paca.Browser; using Paca.Browser.NewTab; using Paca.App.Browser.Features; using Paca.Tests.Infrastructure; using Xunit; namespace Paca.Tests.Browser; /// /// 크롬 스타일 홈 (새 탭 / NTP) 기능 단위 및 통합 테스트: /// 1. NtpModels & NtpStore — 바로가기 CRUD, 순서 정렬, 방문기록 기반 자주 찾는 사이트 추출, 맞춤설정(테마/배경/위젯) 영속화 /// 2. NewTabPageBuilder — 로고, 검색창(검색엔진 연동), 바로가기 그리드, "+ 바로가기 추가" 모달, 맞춤설정 서랍, 시계/카드, WebMessage 양방향 브릿지 /// 3. XSS 방어 및 안전한 HTML 생성 검증 /// 4. 크롬 새 탭 URL 식별 (paca://newtab, about:newtab, chrome://newtab) /// public class ChromeNewTabPageTests : IDisposable { private readonly string _testDataDir; public ChromeNewTabPageTests() { _testDataDir = Path.Combine(Path.GetTempPath(), "PacaNtpTest_" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(_testDataDir); NtpStore.CustomDataDir = _testDataDir; } public void Dispose() { NtpStore.CustomDataDir = null; try { if (Directory.Exists(_testDataDir)) Directory.Delete(_testDataDir, recursive: true); } catch { } } [Fact] public void 저장된_바로가기가_없으면_기본_프리셋_바로가기_목록을_반환한다() { var shortcuts = NtpStore.LoadShortcuts(); Assert.NotNull(shortcuts); Assert.NotEmpty(shortcuts); Assert.Contains(shortcuts, s => s.Title.Contains("Google") || s.Url.Contains("google.com")); Assert.Contains(shortcuts, s => s.Title.Contains("YouTube") || s.Url.Contains("youtube.com")); Assert.Contains(shortcuts, s => s.Title.Contains("Naver") || s.Url.Contains("naver.com")); Assert.Contains(shortcuts, s => s.Title.Contains("GitHub") || s.Url.Contains("github.com")); } [Fact] public void 바로가기_추가_수정_삭제_기능이_정상_동작하고_JSON_파일에_저장된다() { // 1. 추가 var added = NtpStore.AddShortcut("테스트 사이트", "https://example.org/test"); Assert.NotNull(added); Assert.Equal("테스트 사이트", added.Title); Assert.Equal("https://example.org/test", added.Url); Assert.False(string.IsNullOrWhiteSpace(added.Id)); var loaded = NtpStore.LoadShortcuts(); Assert.Contains(loaded, s => s.Id == added.Id && s.Title == "테스트 사이트"); // 2. 수정 var updated = NtpStore.EditShortcut(added.Id, "수정된 사이트", "https://updated.org"); Assert.True(updated); var loadedAfterEdit = NtpStore.LoadShortcuts(); var editedItem = loadedAfterEdit.First(s => s.Id == added.Id); Assert.Equal("수정된 사이트", editedItem.Title); Assert.Equal("https://updated.org", editedItem.Url); // 3. 삭제 var deleted = NtpStore.RemoveShortcut(added.Id); Assert.True(deleted); var loadedAfterDelete = NtpStore.LoadShortcuts(); Assert.DoesNotContain(loadedAfterDelete, s => s.Id == added.Id); } [Fact] public void 바로가기_URL에_프로토콜이_없으면_https가_자동_부착된다() { var added = NtpStore.AddShortcut("깃허브", "github.com/myrepo"); Assert.StartsWith("https://", added.Url); Assert.Equal("https://github.com/myrepo", added.Url); } [Fact] public void 바로가기_순서_재정렬이_정상_반영된다() { var shortcuts = NtpStore.LoadShortcuts(); Assert.True(shortcuts.Count >= 3); var id0 = shortcuts[0].Id; var id1 = shortcuts[1].Id; var id2 = shortcuts[2].Id; // 순서 변경: id2, id0, id1 var newOrder = new List { id2, id0, id1 }; var success = NtpStore.ReorderShortcuts(newOrder); Assert.True(success); var reordered = NtpStore.LoadShortcuts(); Assert.Equal(id2, reordered[0].Id); Assert.Equal(id0, reordered[1].Id); Assert.Equal(id1, reordered[2].Id); } [Fact] public void 맞춤설정_테마_배경_위젯_설정_저장_및_로드가_정상_동작한다() { var defaultSettings = NtpStore.LoadSettings(); Assert.NotNull(defaultSettings); Assert.Equal("custom", defaultSettings.ShortcutsMode); var customSettings = new NtpSettings { ShortcutsMode = "frequent", ThemeColor = "ocean", Wallpaper = "aurora", CustomWallpaperUrl = "https://images.unsplash.com/photo-test", ShowRecentCards = false, ShowDownloadsCard = true, ShowClock = true, SearchEngineName = "Naver" }; NtpStore.SaveSettings(customSettings); var reloaded = NtpStore.LoadSettings(); Assert.Equal("frequent", reloaded.ShortcutsMode); Assert.Equal("ocean", reloaded.ThemeColor); Assert.Equal("aurora", reloaded.Wallpaper); Assert.Equal("https://images.unsplash.com/photo-test", reloaded.CustomWallpaperUrl); Assert.False(reloaded.ShowRecentCards); Assert.True(reloaded.ShowDownloadsCard); Assert.True(reloaded.ShowClock); Assert.Equal("Naver", reloaded.SearchEngineName); } [Fact] public void 방문_기록으로부터_자주_방문한_사이트_목록을_도메인별_빈도_기준으로_추출한다() { var history = new List { new("유튜브 메인", "https://youtube.com", DateTime.Now.AddHours(-1)), new("유튜브 영상1", "https://youtube.com/watch?v=1", DateTime.Now.AddHours(-2)), new("유튜브 영상2", "https://youtube.com/watch?v=2", DateTime.Now.AddHours(-3)), new("네이버 메인", "https://naver.com", DateTime.Now.AddHours(-4)), new("네이버 뉴스", "https://news.naver.com/article", DateTime.Now.AddHours(-5)), new("구글 검색", "https://google.com/search?q=test", DateTime.Now.AddHours(-6)), new("내부 설정", "about:blank", DateTime.Now.AddHours(-7)), new("무효 URL", "javascript:void(0)", DateTime.Now.AddHours(-8)), }; var topSites = NtpStore.GetTopSitesFromHistory(history, max: 4); Assert.NotEmpty(topSites); Assert.True(topSites.Count <= 4); // 빈도 1위: youtube.com (3회) Assert.Contains("youtube.com", topSites[0].Url); // 빈도 2위: naver.com (2회) Assert.Contains("naver.com", topSites[1].Url); // 무효 URL이나 about:blank 는 제외됨 Assert.DoesNotContain(topSites, s => s.Url.StartsWith("about:") || s.Url.StartsWith("javascript:")); } [Fact] public void NewTabPageBuilder는_크롬_홈화면_핵심_요소들을_모두_포함한_HTML을_생성한다() { var shortcuts = new List { new("1", "구글", "https://google.com", IsCustom: true), new("2", "유튜브", "https://youtube.com", IsCustom: true) }; var settings = new NtpSettings { ShortcutsMode = "custom", ThemeColor = "default", Wallpaper = "default", ShowRecentCards = true, ShowClock = true, SearchEngineName = "Google" }; var recentHistory = new List { new("최근 페이지 1", "https://example.com/1", DateTime.Now.AddMinutes(-10)) }; var recentDownloads = new List { new("video.mp4", "https://sample.com/video.mp4", 1024 * 1024 * 50, true, DateTime.Now.AddMinutes(-5)) }; var html = NewTabPageBuilder.BuildNtp( shortcuts, settings, recentHistory, recentDownloads, isDark: true, accentColor: "#6c5ce7", fontStack: "Malgun Gothic, sans-serif"); Assert.NotNull(html); Assert.NotEmpty(html); // 1. 크롬 로고 / 타이틀 브랜드 Assert.Contains("google-logo", html); Assert.Contains("Google", html); // 2. 옴니박스 검색창 (검색엔진 플레이스홀더 및 입력창) Assert.Contains("id=\"search-input\"", html); Assert.Contains("Google에서 검색하거나 URL을 입력하세요", html); // 3. 바로가기 그리드 & 바로가기 추가 버튼 Assert.Contains("class=\"shortcut-tile\"", html); Assert.Contains("구글", html); Assert.Contains("유튜브", html); Assert.Contains("add-shortcut-btn", html); Assert.Contains("바로가기 추가", html); // 4. 바로가기 추가/수정 모달 다이얼로그 Assert.Contains("id=\"shortcut-modal\"", html); Assert.Contains("id=\"modal-title-input\"", html); Assert.Contains("id=\"modal-url-input\"", html); // 5. 맞춤설정 (Customize Chrome) 플로팅 버튼 & 슬라이드 서랍 Assert.Contains("id=\"customize-btn\"", html); Assert.Contains("맞춤설정", html); Assert.Contains("id=\"customize-drawer\"", html); Assert.Contains("배경", html); Assert.Contains("테마 색상", html); // 6. 시계 및 인사말 위젯 Assert.Contains("id=\"clock-widget\"", html); Assert.Contains("id=\"clock-time\"", html); // 7. 최근 활동 카드 (방문 기록 & 다운로드) Assert.Contains("id=\"recent-cards\"", html); Assert.Contains("최근 방문한 페이지", html); Assert.Contains("최근 다운로드", html); Assert.Contains("video.mp4", html); // 8. WebView2 WebMessage 브릿지 스크립트 Assert.Contains("window.chrome.webview.postMessage", html); Assert.Contains("ntp:search", html); Assert.Contains("ntp:navigate", html); Assert.Contains("ntp:addShortcut", html); Assert.Contains("ntp:editShortcut", html); Assert.Contains("ntp:removeShortcut", html); Assert.Contains("ntp:saveSettings", html); } [Fact] public void NewTabPageBuilder는_사용자_입력_특수문자_XSS를_이스케이프_한다() { var xssShortcuts = new List { new("xss1", "", "https://test.com/\"'onmouseover='alert(1)") }; var settings = new NtpSettings(); var html = NewTabPageBuilder.BuildNtp(xssShortcuts, settings, isDark: true); // 스크립트 태그가 그대로 실행 코드로 노출되지 않아야 함 Assert.DoesNotContain("", html); Assert.Contains("<script>alert('xss')</script>", html); } [Theory] [InlineData("paca://newtab", true)] [InlineData("about:newtab", true)] [InlineData("chrome://newtab", true)] [InlineData("paca://home", true)] [InlineData("https://www.google.com", false)] [InlineData("https://example.com", false)] public void IsNewTabUrl은_크롬_및_파카_새탭_스킴을_정확히_판정한다(string url, bool expected) { var isNewTab = NewTabPageBuilder.IsNewTabUrl(url); Assert.Equal(expected, isNewTab); } [Fact] public void MainWindow_초기화_시_새탭_웹메시지_수신_이벤트가_바인딩되어_있다() { var cs = SourceTree.MainWindowCs; Assert.Contains("_views.NtpMessageReceived +=", cs); Assert.Contains("HandleNtpMessage", cs); } }