using System.IO;
using System.Text;
using Paca.App.Browser.Features;
using Paca.Browser;
using Paca.Browser.PageControls;
using Paca.Core;
using Paca.Core.Config;
using Xunit;
namespace Paca.Tests.Browser;
///
/// 브라우저 권한 제어, 추적 차단, URL 정제, 사이트 줌/컨트롤 파이프라인.
///
/// T3-11: 사이트별 권한 정책 — 기억된 상태 우선, 미기억은 Ask(프롬프트).
public class PermissionGateTests
{
[Fact]
public void 미기억_권한은_Ask()
=> Assert.Equal(PermissionVerdict.Ask, new PermissionGate().Decide("notifications", "https://x.com"));
[Theory]
[InlineData("allow")]
[InlineData("deny")]
public void 기억된_판정은_그대로_반환(string v)
{
var g = new PermissionGate();
g.Remember("notifications", "https://x.com", v == "allow" ? PermissionVerdict.Allow : PermissionVerdict.Deny);
Assert.Equal(v == "allow" ? PermissionVerdict.Allow : PermissionVerdict.Deny,
g.Decide("notifications", "https://x.com"));
}
[Fact]
public void 판정은_출처_단위_격리()
{
var g = new PermissionGate();
g.Remember("geolocation", "https://a.com", PermissionVerdict.Allow);
Assert.Equal(PermissionVerdict.Ask, g.Decide("geolocation", "https://b.com"));
}
[Fact]
public void 판정은_종류_단위_격리()
{
var g = new PermissionGate();
g.Remember("notifications", "https://a.com", PermissionVerdict.Allow);
Assert.Equal(PermissionVerdict.Ask, g.Decide("camera", "https://a.com"));
}
[Fact]
public void OtherSensors_등_민감_종류는_자동_거절()
=> Assert.Equal(PermissionVerdict.Deny, new PermissionGate().Decide("OtherSensors", "https://x.com"));
[Fact]
public void MultipleAutomaticDownloads_파일_다운로드는_자동_허용()
=> Assert.Equal(PermissionVerdict.Allow, new PermissionGate().Decide("MultipleAutomaticDownloads", "https://x.com"));
}
/// T3-14: 추적 방지 — 블랙리스트 도메인 매칭(서브도메인 포함) + 차단 카운터.
public class TrackerBlockTests
{
[Theory]
[InlineData("https://google-analytics.com/analytics.js", true)] // 정확히 도메인
[InlineData("https://www.google-analytics.com/g/collect", true)] // 서브도메인
[InlineData("https://x.com/notgoogle-analytics.com/a", false)] // 접미사 함정 제외
[InlineData("https://doubleclick.net/instream/ad_status.js", true)]
[InlineData("https://safe.example.com/lib.js", false)] // 무관 도메인
public void Matches_블랙리스트_도메인_판정(string url, bool expected)
=> Assert.Equal(expected, new TrackerBlocklist().Matches(new Uri(url)));
[Fact]
public void 커스텀_도메인_주입_가능()
{
var bl = new TrackerBlocklist(new[] { "blocked.test" });
Assert.True(bl.Matches(new Uri("https://t.blocked.test/x.js")));
Assert.False(bl.Matches(new Uri("https://ok.com/x.js")));
}
[Fact]
public void Counter_차단_누적_도메인별_집계_변경_통지()
{
var c = new TrackerCounter();
var changed = 0;
c.Changed += () => changed++;
Assert.Equal(0, c.Total);
c.Record("https://www.google-analytics.com/a.js");
c.Record("https://google-analytics.com/b.js");
c.Record("https://scorecardresearch.com/c.js");
Assert.Equal(3, c.Total);
Assert.Equal(2, c.ByDomain["google-analytics.com"]);
Assert.Equal(1, c.ByDomain["scorecardresearch.com"]);
Assert.Equal(3, changed);
}
}
/// T3-15: 파라미터 제거 복사 + 주소창 진행 상태.
public class UrlCleanerTests
{
[Theory]
[InlineData("https://x.com/a?utm_source=1&b=2", "https://x.com/a")] // 쿼리 제거
[InlineData("https://x.com/a#frag", "https://x.com/a")] // 프래그먼트 제거
[InlineData("https://x.com/a?b=1#f", "https://x.com/a")] // 둘 다
[InlineData("https://x.com/a/", "https://x.com/a/")] // 없으면 무변경
[InlineData("https://x.com/a?q=한글", "https://x.com/a")] // 유니코드 쿼리
[InlineData("x.com/a?b=1", "x.com/a")] // 스킴 없어도 동작
public void StripQuery_파라미터와_프래그먼트를_제거(string input, string expected)
=> Assert.Equal(expected, UrlCleaner.StripQuery(input));
[Fact]
public void StripQuery_빈값_null_안전()
{
Assert.Equal("", UrlCleaner.StripQuery(""));
Assert.Null(UrlCleaner.StripQuery(null));
}
}
/// T3-15: 탭 전체 네비게이션 진행 집계 — 주소창 진행 바 표시 상태.
public class NavProgressTrackerTests
{
[Fact]
public void 초기값은_비진행()
=> Assert.False(new NavProgressTracker().IsNavigating);
[Fact]
public void 시작하면_진행()
{
var t = new NavProgressTracker();
t.Started();
Assert.True(t.IsNavigating);
}
[Fact]
public void 완료되면_비진행()
{
var t = new NavProgressTracker();
t.Started();
t.Completed();
Assert.False(t.IsNavigating);
}
[Fact]
public void 동시_내비게이션은_마지막_완료까지_진행() // 프레임 리다이렉트·중복 시작
{
var t = new NavProgressTracker();
t.Started(); t.Started();
t.Completed();
Assert.True(t.IsNavigating);
t.Completed();
Assert.False(t.IsNavigating);
}
[Fact]
public void 과잉_완료로_음수_되지_않음()
{
var t = new NavProgressTracker();
t.Completed();
Assert.False(t.IsNavigating);
}
[Fact]
public void IsNavigating_변경_시_INPC_발화()
{
var t = new NavProgressTracker();
var fired = new List();
t.PropertyChanged += (_, e) => fired.Add(e.PropertyName);
t.Started();
t.Completed();
t.Completed(); // false→false → 이벤트 없음
Assert.Equal(new[] { nameof(NavProgressTracker.IsNavigating), nameof(NavProgressTracker.IsNavigating) }, fired);
}
}
// T1-7 사이트별 줌 영속: 도메인→배율 저장, 범위 클램프, JSON 영속, 최대 항목 제한.
public class SiteZoomStoreTests : IDisposable
{
private readonly string _path;
private readonly SiteZoomStore _store;
public SiteZoomStoreTests()
{
_path = Path.Combine(Path.GetTempPath(), $"zoomstore_{Guid.NewGuid():N}.json");
_store = new SiteZoomStore(_path);
}
public void Dispose()
{
try { File.Delete(_path); } catch { }
}
[Fact]
public void 미등록_사이트는_기본값_1_0을_반환한다()
{
Assert.Equal(1.0, _store.Get("https://example.com/page"));
}
[Fact]
public void 배율을_저장하면_같은_호스트의_다른_경로에서도_읽힌다()
{
_store.Set("https://example.com/a", 1.5);
Assert.Equal(1.5, _store.Get("https://example.com/b/c/d"));
}
[Fact]
public void www_접두사는_동일_호스트로_정규화된다()
{
_store.Set("https://www.example.com/a", 2.0);
Assert.Equal(2.0, _store.Get("https://example.com/b"));
}
[Theory]
[InlineData(0.05, 0.1)] // 하한 클램프
[InlineData(9.0, 5.0)] // 상한 클램프
[InlineData(1.25, 1.25)] // 정상 범위
public void 배율은_0_1에서_5_0으로_클램프된다(double input, double expected)
{
_store.Set("https://clamp.com/", input);
Assert.Equal(expected, _store.Get("https://clamp.com/"));
}
[Fact]
public void 저장_즉시_파일에_영속되어_새_인스턴스가_읽는다()
{
_store.Set("https://persist.com/", 1.75);
Assert.True(File.Exists(_path));
var reloaded = new SiteZoomStore(_path);
Assert.Equal(1.75, reloaded.Get("https://persist.com/other"));
}
[Fact]
public void 최대_300개_초과시_가장_오래_사용하지_한_항목부터_제거된다()
{
// 300개 채우기
for (int i = 0; i < 300; i++)
_store.Set($"https://site{i}.com/", 1.0 + i * 0.001);
// 가장 먼저 넣은 site0 을 최근에 다시 설정(LRU 갱신) → site1 이 제거 후보가 됨
_store.Set("https://site0.com/", 1.9);
// 하나 더 추가 → 초과분 제거
_store.Set("https://new.com/", 2.0);
Assert.Equal(300, _store.Count);
Assert.Equal(1.0, _store.Get("https://site1.com/")); // 제거됨 → 기본값
Assert.Equal(2.0, _store.Get("https://new.com/")); // 유지
Assert.Equal(1.9, _store.Get("https://site0.com/")); // LRU 갱신됨 → 유지
}
[Fact]
public void Clear는_모든_항목과_파일을_지운다()
{
_store.Set("https://x.com/", 2.0);
_store.Clear();
Assert.Equal(0, _store.Count);
Assert.Equal(1.0, _store.Get("https://x.com/"));
Assert.False(File.Exists(_path));
}
[Fact]
public void 손상된_JSON은_무시하고_빈_상태로_시작한다()
{
File.WriteAllText(_path, "{ not json !!");
var store = new SiteZoomStore(_path);
Assert.Equal(0, store.Count);
}
}
public class PageControlsManagerTests
{
[Fact]
public void ExtractDomain_ReturnsCorrectHost()
{
Assert.Equal("www.youtube.com", PageControlsManager.ExtractDomain("https://www.youtube.com/watch?v=12345"));
Assert.Equal("github.com", PageControlsManager.ExtractDomain("https://github.com/microsoft/webview2"));
Assert.Equal("", PageControlsManager.ExtractDomain("about:blank"));
Assert.Equal("", PageControlsManager.ExtractDomain("invalid-url"));
}
[Fact]
public void IsSecureScheme_DetectsHttps()
{
Assert.True(PageControlsManager.IsSecureScheme("https://youtube.com"));
Assert.False(PageControlsManager.IsSecureScheme("http://insecure-site.com"));
Assert.False(PageControlsManager.IsSecureScheme("about:blank"));
}
[Fact]
public void Settings_StoreAndResetCorrectly()
{
var manager = new PageControlsManager();
var domain = "youtube.com";
var initial = manager.GetSettings(domain);
Assert.True(initial.SoundAllowed);
Assert.True(initial.TrackerBlocking);
manager.SetSoundAllowed(domain, false);
manager.SetTrackerBlocking(domain, false);
manager.SetDarkModeBoost(domain, true);
var modified = manager.GetSettings(domain);
Assert.False(modified.SoundAllowed);
Assert.False(modified.TrackerBlocking);
Assert.True(modified.DarkModeBoost);
manager.ResetSettings(domain);
var reset = manager.GetSettings(domain);
Assert.True(reset.SoundAllowed);
Assert.True(reset.TrackerBlocking);
Assert.False(reset.DarkModeBoost);
}
}
/// 페이지 다운로드 추상화(다운로드 매니저 UI 의 로직 코어) — fake op 로 검증.
public interface IFakeOpHost { }
public class PageDownloadTrackerTests
{
private sealed class FakeOp : IDownloadOperation
{
public string Uri { get; set; } = "https://x.test/file.zip";
public string FileName { get; set; } = "file.zip";
public long BytesReceived { get; set; }
public long TotalBytesToReceive { get; set; } = 1000;
public string State { get; set; } = "InProgress";
public bool Paused, Resumed, Cancelled;
public event Action? Changed;
public void Raise() => Changed?.Invoke();
public void Pause() { Paused = true; State = "Paused"; Raise(); }
public void Resume() { Resumed = true; State = "InProgress"; Raise(); }
public void Cancel() { Cancelled = true; State = "Cancelled"; Raise(); }
}
[Fact]
public void Add는_항목을_등록하고_최근순_정렬한다()
{
var t = new PageDownloadTracker();
var a = new FakeOp { FileName = "a.zip" };
var b = new FakeOp { FileName = "b.zip" };
t.Add(a, a.FileName);
t.Add(b, b.FileName);
Assert.Equal(2, t.Items.Count);
Assert.Equal("b.zip", t.Items[0].FileName); // 최신 우선
}
[Fact]
public void 진행률과_속도를_계산한다()
{
var t = new PageDownloadTracker();
var op = new FakeOp { TotalBytesToReceive = 1000 };
var item = t.Add(op, "f.zip");
item.UpdateSpeed(500, TimeSpan.FromSeconds(2)); // 500B/2s = 250 B/s
Assert.Equal(50.0, item.Percent, 2);
Assert.Equal(250, item.BytesPerSecond);
Assert.Equal("250.0 B/s", item.SpeedText);
}
[Fact]
public void 속도_단위는_KB_MB로_표시된다()
{
var t = new PageDownloadTracker();
var item = t.Add(new FakeOp { TotalBytesToReceive = 10_000_000 }, "f");
item.UpdateSpeed(0, TimeSpan.FromSeconds(1)); // 초기
Assert.Equal("—", item.SpeedText);
item = t.Add(new FakeOp { TotalBytesToReceive = 10_000_000 }, "g");
item.UpdateSpeed(2048 * 100, TimeSpan.FromSeconds(1)); // 200 KB/s
Assert.Equal("200.0 KB/s", item.SpeedText);
item = t.Add(new FakeOp { TotalBytesToReceive = 10_000_000 }, "h");
item.UpdateSpeed(3 * 1024 * 1024L, TimeSpan.FromSeconds(1)); // 3 MB/s
Assert.Equal("3.0 MB/s", item.SpeedText);
}
[Fact]
public void Pause_Resume_Cancel_위임()
{
var t = new PageDownloadTracker();
var op = new FakeOp();
var item = t.Add(op, "f");
t.Pause(item); t.Resume(item); t.Cancel(item);
Assert.True(op.Paused && op.Resumed && op.Cancelled);
}
[Fact]
public void 완료_항목은_ClearCompleted로_일괄_제거()
{
var t = new PageDownloadTracker();
var done = new FakeOp { State = "Completed" };
var running = new FakeOp { State = "InProgress" };
t.Add(done, "d"); t.Add(running, "r");
t.ClearCompleted();
Assert.Single(t.Items);
Assert.Equal("r", t.Items[0].FileName);
}
[Fact]
public void 취소된_항목도_ClearCompleted로_제거된다()
{
var t = new PageDownloadTracker();
var c = new FakeOp { State = "Interrupted" };
t.Add(c, "c");
t.ClearCompleted();
Assert.Empty(t.Items);
}
}