T3-11/14/15 + M2 잔여: 권한 정책 UI(PermissionGate/PermissionRequested 다이얼로그+SavesInProfile), 추적 방지(TrackerBlocklist/WebResourceRequested 차단+실드 배지·팝업), 주소창 진행 바+파라미터 제거 복사(UrlCleaner/NavProgressTracker), U5 완료 알림(Mobile.Core 감지기+Plugin.LocalNotification), 재생중 ongoing 미디어 알림, 썸네일/해상도 API(Thumbnailer 사이드카 캐시+/thumb)+라이브러리 그리드, 외부망 안내(ConnectionTroubleshoot). 147 tests GREEN
This commit is contained in:
parent
27c91c04dd
commit
46c6c321f2
26 changed files with 1058 additions and 30 deletions
46
VideoDownloader.Tests/Browser/PermissionGateTests.cs
Normal file
46
VideoDownloader.Tests/Browser/PermissionGateTests.cs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
using VideoDownloader.Browser;
|
||||
|
||||
namespace VideoDownloader.Tests.Browser;
|
||||
|
||||
/// <summary>T3-11: 사이트별 권한 정책 — 기억된 상태 우선, 미기억은 Ask(프롬프트).</summary>
|
||||
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"));
|
||||
}
|
||||
40
VideoDownloader.Tests/Browser/TrackerBlockTests.cs
Normal file
40
VideoDownloader.Tests/Browser/TrackerBlockTests.cs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
using VideoDownloader.Browser;
|
||||
|
||||
namespace VideoDownloader.Tests.Browser;
|
||||
|
||||
/// <summary>T3-14: 추적 방지 — 블랙리스트 도메인 매칭(서브도메인 포함) + 차단 카운터.</summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
80
VideoDownloader.Tests/Browser/UrlCleanerTests.cs
Normal file
80
VideoDownloader.Tests/Browser/UrlCleanerTests.cs
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
using VideoDownloader.Browser;
|
||||
|
||||
namespace VideoDownloader.Tests.Browser;
|
||||
|
||||
/// <summary>T3-15: 파라미터 제거 복사 + 주소창 진행 상태.</summary>
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>T3-15: 탭 전체 네비게이션 진행 집계 — 주소창 진행 바 표시 상태.</summary>
|
||||
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<string?>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
109
VideoDownloader.Tests/E2E/PermissionTrackerE2ETests.cs
Normal file
109
VideoDownloader.Tests/E2E/PermissionTrackerE2ETests.cs
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
using System.Windows;
|
||||
using System.Windows.Threading;
|
||||
using Microsoft.Web.WebView2.Core;
|
||||
using VideoDownloader.Browser;
|
||||
using VideoDownloader.Tests.Infrastructure;
|
||||
|
||||
namespace VideoDownloader.Tests.E2E;
|
||||
|
||||
/// <summary>
|
||||
/// E2E (T3-11/T3-14): 실제 WebView2 에서 권한 정책 응답과 추적 요청 차단·집계를 검증.
|
||||
/// TabViewService 와 동일한 배선(PermissionGate/TrackerBlocklist/TrackerCounter)을 사용한다.
|
||||
/// </summary>
|
||||
[Collection("E2E")]
|
||||
public class PermissionTrackerE2ETests
|
||||
{
|
||||
[Fact]
|
||||
public async Task 권한_요청이_정책을_거쳐_허용된다_T3_11()
|
||||
{
|
||||
using var sta = StaDispatcher.Start();
|
||||
var udf = Path.Combine(Path.GetTempPath(), $"vd_e2e_{Guid.NewGuid():N}");
|
||||
var result = await sta.InvokeAsync(async () =>
|
||||
{
|
||||
var window = new Window();
|
||||
var browser = new Microsoft.Web.WebView2.Wpf.WebView2();
|
||||
window.Content = browser;
|
||||
window.Show();
|
||||
|
||||
var env = await CoreWebView2Environment.CreateAsync(userDataFolder: udf);
|
||||
await browser.EnsureCoreWebView2Async(env);
|
||||
var cv = browser.CoreWebView2;
|
||||
|
||||
// TabViewService 배선과 동일: 기억된 판정은 즉시 응답(출처는 이벤트에서 관측된 것으로 기억)
|
||||
var gate = new PermissionGate();
|
||||
cv.PermissionRequested += (_, e) =>
|
||||
{
|
||||
var o = VideoDownloader.App.Services.TabViewService.OriginOf(e.Uri);
|
||||
gate.Remember("Notifications", o, PermissionVerdict.Allow); // E2E: 알림은 허용 정책
|
||||
var verdict = gate.Decide(e.PermissionKind.ToString(), o);
|
||||
e.State = verdict == PermissionVerdict.Allow ? CoreWebView2PermissionState.Allow : CoreWebView2PermissionState.Deny;
|
||||
e.Handled = true;
|
||||
};
|
||||
|
||||
var html = "<html><body><script>Notification.requestPermission().then(p => { document.title = 'PERM=' + p; });</script></body></html>";
|
||||
cv.NavigateToString(html);
|
||||
await WaitUntilNavCompleted(cv);
|
||||
var title = await Wait.Until(() => Task.FromResult(cv.DocumentTitle ?? ""),
|
||||
t => t.StartsWith("PERM=", StringComparison.Ordinal), 10000);
|
||||
|
||||
window.Close();
|
||||
return title;
|
||||
});
|
||||
|
||||
try { Directory.Delete(udf, true); } catch { }
|
||||
Assert.Equal("PERM=granted", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task 블랙리스트_스크립트가_차단되고_카운터에_집계된다_T3_14()
|
||||
{
|
||||
using var sta = StaDispatcher.Start();
|
||||
var udf = Path.Combine(Path.GetTempPath(), $"vd_e2e_{Guid.NewGuid():N}");
|
||||
var result = await sta.InvokeAsync(async () =>
|
||||
{
|
||||
var window = new Window();
|
||||
var browser = new Microsoft.Web.WebView2.Wpf.WebView2();
|
||||
window.Content = browser;
|
||||
window.Show();
|
||||
|
||||
var env = await CoreWebView2Environment.CreateAsync(userDataFolder: udf);
|
||||
await browser.EnsureCoreWebView2Async(env);
|
||||
var cv = browser.CoreWebView2;
|
||||
|
||||
var bl = new TrackerBlocklist(new[] { "blocked.test" });
|
||||
var counter = new TrackerCounter();
|
||||
cv.AddWebResourceRequestedFilter("*", CoreWebView2WebResourceContext.All);
|
||||
cv.WebResourceRequested += (_, e) =>
|
||||
{
|
||||
if (!bl.Matches(new Uri(e.Request.Uri))) return;
|
||||
e.Response = env.CreateWebResourceResponse(null, 204, "Blocked", "");
|
||||
counter.Record(e.Request.Uri);
|
||||
};
|
||||
|
||||
var html = "<html><body><p>tracker-e2e</p>" +
|
||||
"<script src=\"https://www.blocked.test/a.js\"></script>" +
|
||||
"<script src=\"https://www.blocked.test/b.js\"></script>" +
|
||||
"<script src=\"https://ok.example.com/c.js\"></script>" +
|
||||
"</body></html>";
|
||||
cv.NavigateToString(html);
|
||||
await WaitUntilNavCompleted(cv);
|
||||
await Wait.Until(() => Task.FromResult(counter.Total), t => t >= 2, 10000);
|
||||
|
||||
window.Close();
|
||||
return counter.Total;
|
||||
});
|
||||
|
||||
try { Directory.Delete(udf, true); } catch { }
|
||||
Assert.Equal(2, result); // 정상 도메인(ok.example.com)은 집계되지 않음
|
||||
}
|
||||
|
||||
private static async Task WaitUntilNavCompleted(CoreWebView2 cv)
|
||||
{
|
||||
var tcs = new TaskCompletionSource();
|
||||
EventHandler<CoreWebView2NavigationCompletedEventArgs> h = (_, _) => tcs.TrySetResult();
|
||||
cv.NavigationCompleted += h;
|
||||
if (tcs.Task.IsCompleted) return;
|
||||
await Task.WhenAny(tcs.Task, Task.Delay(15000));
|
||||
cv.NavigationCompleted -= h;
|
||||
}
|
||||
}
|
||||
106
VideoDownloader.Tests/Mobile/MobileCoreTests.cs
Normal file
106
VideoDownloader.Tests/Mobile/MobileCoreTests.cs
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
using VideoDownloader.Mobile;
|
||||
|
||||
namespace VideoDownloader.Tests.Mobile;
|
||||
|
||||
/// <summary>Mobile.Core 순수 로직 — U5 완료 감지, 외부망 안내, U4 저장소(파일 시스템).</summary>
|
||||
public class MobileCoreTests
|
||||
{
|
||||
// ── U5 완료 알림 감지 ──
|
||||
|
||||
[Fact]
|
||||
public void 완료_전환_항목_ID_반환()
|
||||
{
|
||||
var d = new QueueCompletionDetector();
|
||||
var prev = new[] { ("a", "다운로드 중"), ("b", "대기") };
|
||||
var next = new[] { ("a", "완료"), ("b", "다운로드 중") };
|
||||
Assert.Equal(new[] { "a" }, d.Detect(prev, next));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void 이미_완료였던_항목은_재알림_없음()
|
||||
{
|
||||
var d = new QueueCompletionDetector();
|
||||
var prev = new[] { ("a", "완료") };
|
||||
var next = new[] { ("a", "완료") };
|
||||
Assert.Empty(d.Detect(prev, next));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void 첫_폴링에서_이미_완료된_항목은_무시() // 앱 시작 시 과거 완료 스팸 방지
|
||||
{
|
||||
var d = new QueueCompletionDetector();
|
||||
Assert.Empty(d.Detect(Array.Empty<(string, string)>(), new[] { ("a", "완료") }));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void 목록에서_사라진_항목은_알림_없음()
|
||||
{
|
||||
var d = new QueueCompletionDetector();
|
||||
var prev = new[] { ("a", "다운로드 중") };
|
||||
Assert.Empty(d.Detect(prev, Array.Empty<(string, string)>()));
|
||||
}
|
||||
|
||||
// ── 외부망 안내(MOBILE_PLAN §7) ──
|
||||
|
||||
[Fact]
|
||||
public void 미연결_안내()
|
||||
{
|
||||
var m = ConnectionTroubleshoot.Message(isConfigured: false, lastError: null);
|
||||
Assert.Contains("연결", m);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void 연결됐지만_실패_시_같은네트워크_VPN_안내()
|
||||
{
|
||||
var m = ConnectionTroubleshoot.Message(isConfigured: true, lastError: "No such host is known.");
|
||||
Assert.Contains("같은 네트워크", m);
|
||||
Assert.Contains("Tailscale", m); // 계획 §3: 자체 릴레이 대신 Tailscale 문서화
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void 정상_시_빈_문자열()
|
||||
=> Assert.Equal("", ConnectionTroubleshoot.Message(isConfigured: true, lastError: null));
|
||||
|
||||
// ── U4 저장소(임시 디렉터리로 파일 시스템 검증) ──
|
||||
|
||||
[Fact]
|
||||
public void OfflineStore_기록_목록_삭제_라운드트립()
|
||||
{
|
||||
var baseDir = Path.Combine(Path.GetTempPath(), $"vd_core_{Guid.NewGuid():N}");
|
||||
var store = new OfflineStore(baseDir);
|
||||
Assert.Empty(store.List());
|
||||
|
||||
var p = store.PathFor("id1");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(p)!);
|
||||
File.WriteAllBytes(p, new byte[123]);
|
||||
store.Record("id1", "video.mp4");
|
||||
|
||||
Assert.True(store.Exists("id1"));
|
||||
var list = store.List();
|
||||
Assert.Single(list);
|
||||
Assert.Equal(("id1", "video.mp4", 123L), (list[0].Id, list[0].FileName, list[0].SizeBytes));
|
||||
|
||||
store.Delete("id1");
|
||||
Assert.False(store.Exists("id1"));
|
||||
Assert.Empty(store.List());
|
||||
|
||||
Directory.Delete(baseDir, true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OfflineStore_재시작_시_매니페스트_유지()
|
||||
{
|
||||
var baseDir = Path.Combine(Path.GetTempPath(), $"vd_core_{Guid.NewGuid():N}");
|
||||
var store = new OfflineStore(baseDir);
|
||||
var p = store.PathFor("id2");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(p)!);
|
||||
File.WriteAllBytes(p, new byte[10]);
|
||||
store.Record("id2", "keep.mp4");
|
||||
|
||||
var reopened = new OfflineStore(baseDir); // 새 인스턴스 = 앱 재시작
|
||||
Assert.True(reopened.Exists("id2"));
|
||||
Assert.Equal("keep.mp4", reopened.List().Single().FileName);
|
||||
|
||||
Directory.Delete(baseDir, true);
|
||||
}
|
||||
}
|
||||
|
|
@ -155,6 +155,44 @@ public class RemoteServerE2ETests : IDisposable
|
|||
Assert.Equal(93.5, persisted!.PositionSec);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task 썸네일_API_E2E_생성_캐시_404()
|
||||
{
|
||||
Directory.CreateDirectory(_libDir);
|
||||
var mediaPath = Path.Combine(_libDir, "clip.mp4");
|
||||
File.WriteAllBytes(mediaPath, new byte[2048]);
|
||||
|
||||
// 가짜 ffmpeg: 출력 경로에 JPEG 생성
|
||||
var fake = new Tests.Platform.FakeProcessRunner
|
||||
{
|
||||
Behavior = (spec, _, _) =>
|
||||
{
|
||||
File.WriteAllBytes(spec.Args[^1], new byte[] { 0xFF, 0xD8, 0xFF, 0xE0, 0x01 });
|
||||
return Task.FromResult(new VideoDownloader.Core.Platform.ProcessResult(0, "", ""));
|
||||
},
|
||||
};
|
||||
|
||||
await using var server = await ServerHost.StartAsync(new ServerOptions
|
||||
{
|
||||
Port = 0, Token = "thumb-token", LibraryFolder = _libDir,
|
||||
Queue = new FakeQueue(),
|
||||
Thumbnailer = new Thumbnailer(fake),
|
||||
});
|
||||
using var http = new HttpClient { BaseAddress = new Uri($"http://127.0.0.1:{server.Port}") };
|
||||
|
||||
// 1) 익명 401
|
||||
Assert.Equal(System.Net.HttpStatusCode.Unauthorized, (await http.GetAsync("/api/library/xyz/thumb")).StatusCode);
|
||||
|
||||
// 2) 없는 항목 404
|
||||
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "thumb-token");
|
||||
Assert.Equal(System.Net.HttpStatusCode.NotFound, (await http.GetAsync("/api/library/xyz/thumb")).StatusCode);
|
||||
|
||||
// 3) 실제 항목 → 200 + JPEG 매직
|
||||
var lib = (await http.GetFromJsonAsync<List<LibraryItemDto>>("/api/library"))!;
|
||||
var thumb = await http.GetByteArrayAsync($"/api/library/{lib[0].Id}/thumb");
|
||||
Assert.True(thumb.Length >= 2 && thumb[0] == 0xFF && thumb[1] == 0xD8, "JPEG 매직 바이트");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try { Directory.Delete(_libDir, true); } catch { }
|
||||
|
|
|
|||
84
VideoDownloader.Tests/Server/ThumbnailerTests.cs
Normal file
84
VideoDownloader.Tests/Server/ThumbnailerTests.cs
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
using VideoDownloader.Core.Platform;
|
||||
using VideoDownloader.Server;
|
||||
using VideoDownloader.Tests.Platform;
|
||||
|
||||
namespace VideoDownloader.Tests.Server;
|
||||
|
||||
/// <summary>M2 라이브러리 썸네일/해상도 — ffmpeg 서브프로세스를 Fake 러너로 검증(사이드카 캐시).</summary>
|
||||
public class ThumbnailerTests
|
||||
{
|
||||
private string _dir = "";
|
||||
|
||||
private string NewMedia()
|
||||
{
|
||||
_dir = Path.Combine(Path.GetTempPath(), $"vd_thumb_{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(_dir);
|
||||
var p = Path.Combine(_dir, "clip.mp4");
|
||||
File.WriteAllBytes(p, new byte[100]);
|
||||
return p;
|
||||
}
|
||||
|
||||
private static Task<ProcessResult> FakeFfmpegCreatesJpg(ProcessSpec spec, Action<string, string>? _, CancellationToken __)
|
||||
{
|
||||
// 출력 경로(마지막 인자)에 가짜 JPEG 생성
|
||||
var outPath = spec.Args[^1];
|
||||
File.WriteAllBytes(outPath, new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 }); // JPEG SOI/APP0
|
||||
return Task.FromResult(new ProcessResult(0, "", ""));
|
||||
}
|
||||
|
||||
private static Task<ProcessResult> FakeFfmpegReportsResolution(ProcessSpec spec, Action<string, string>? onLine, CancellationToken _)
|
||||
{
|
||||
var line = " Stream #0:0[0x1](und): Video: h264 (High), yuv420p, 1920x1080 [SAR 1:1 DAR 16:9], 5000 kb/s";
|
||||
onLine?.Invoke("err", line);
|
||||
return Task.FromResult(new ProcessResult(0, "", line));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task 썸네일_생성후_사이드카_캐시로_재실행_없음()
|
||||
{
|
||||
var media = NewMedia();
|
||||
var runner = new FakeProcessRunner { Behavior = FakeFfmpegCreatesJpg };
|
||||
var t = new Thumbnailer(runner);
|
||||
|
||||
var bytes1 = await t.GetThumbnailAsync("ffmpeg", media);
|
||||
Assert.NotNull(bytes1);
|
||||
Assert.True(bytes1!.Length == 4);
|
||||
|
||||
runner.Behavior = (_, _, _) => throw new Xunit.Sdk.XunitException("캐시 히트 시 ffmpeg 재실행 없음");
|
||||
var bytes2 = await t.GetThumbnailAsync("ffmpeg", media); // 캐시
|
||||
Assert.Equal(bytes1, bytes2);
|
||||
Directory.Delete(_dir, true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ffmpeg_실패_시_null()
|
||||
{
|
||||
var media = NewMedia();
|
||||
var runner = new FakeProcessRunner { Behavior = (_, _, _) => Task.FromResult(new ProcessResult(1, "", "boom")) };
|
||||
Assert.Null(await new Thumbnailer(runner).GetThumbnailAsync("ffmpeg", media));
|
||||
Directory.Delete(_dir, true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task 해상도_추출후_사이드카_캐시()
|
||||
{
|
||||
var media = NewMedia();
|
||||
var runner = new FakeProcessRunner { Behavior = FakeFfmpegReportsResolution };
|
||||
var t = new Thumbnailer(runner);
|
||||
|
||||
Assert.Equal("1920x1080", await t.GetResolutionAsync("ffmpeg", media));
|
||||
|
||||
runner.Behavior = (_, _, _) => throw new Xunit.Sdk.XunitException("캐시 히트 시 재실행 없음");
|
||||
Assert.Equal("1920x1080", await t.GetResolutionAsync("ffmpeg", media));
|
||||
Directory.Delete(_dir, true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task 해상기_추출_불가_시_null()
|
||||
{
|
||||
var media = NewMedia();
|
||||
var runner = new FakeProcessRunner { Behavior = (_, _, _) => Task.FromResult(new ProcessResult(0, "", "no streams")) };
|
||||
Assert.Null(await new Thumbnailer(runner).GetResolutionAsync("ffmpeg", media));
|
||||
Directory.Delete(_dir, true);
|
||||
}
|
||||
}
|
||||
|
|
@ -16,5 +16,6 @@
|
|||
<ProjectReference Include="..\VideoDownloader.App\VideoDownloader.App.csproj" />
|
||||
<ProjectReference Include="..\VideoDownloader.Core\VideoDownloader.Core.csproj" />
|
||||
<ProjectReference Include="..\VideoDownloader.Server\VideoDownloader.Server.csproj" />
|
||||
<ProjectReference Include="..\VideoDownloader.Mobile.Core\VideoDownloader.Mobile.Core.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue